코드 리팩토링
재사용성 및 확장성을 고려하여 코드 전반을 리팩토링함
This commit is contained in:
@@ -84,3 +84,18 @@ MonoBehaviour:
|
|||||||
SourcePrefabToOverride: {fileID: 0}
|
SourcePrefabToOverride: {fileID: 0}
|
||||||
SourceHashToOverride: 0
|
SourceHashToOverride: 0
|
||||||
OverridingTargetPrefab: {fileID: 0}
|
OverridingTargetPrefab: {fileID: 0}
|
||||||
|
- Override: 0
|
||||||
|
Prefab: {fileID: 3659626783364531313, guid: 038d8e53e81683f478bd4a50b71cdb82, type: 3}
|
||||||
|
SourcePrefabToOverride: {fileID: 0}
|
||||||
|
SourceHashToOverride: 0
|
||||||
|
OverridingTargetPrefab: {fileID: 0}
|
||||||
|
- Override: 0
|
||||||
|
Prefab: {fileID: 3321405240327640087, guid: 2bb7e098e271eb44a873c856dbf59c7c, type: 3}
|
||||||
|
SourcePrefabToOverride: {fileID: 0}
|
||||||
|
SourceHashToOverride: 0
|
||||||
|
OverridingTargetPrefab: {fileID: 0}
|
||||||
|
- Override: 0
|
||||||
|
Prefab: {fileID: 6403733529880835406, guid: 443aa97110814434cb36b26656f1884c, type: 3}
|
||||||
|
SourcePrefabToOverride: {fileID: 0}
|
||||||
|
SourceHashToOverride: 0
|
||||||
|
OverridingTargetPrefab: {fileID: 0}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ GameObject:
|
|||||||
- component: {fileID: 6108920277980214130}
|
- component: {fileID: 6108920277980214130}
|
||||||
- component: {fileID: 8496505565574430929}
|
- component: {fileID: 8496505565574430929}
|
||||||
- component: {fileID: 6537968746546598605}
|
- component: {fileID: 6537968746546598605}
|
||||||
|
- component: {fileID: 5420652907638236344}
|
||||||
|
- component: {fileID: 1005129893123536417}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: Core
|
m_Name: Core
|
||||||
m_TagString: Core
|
m_TagString: Core
|
||||||
@@ -68,8 +70,6 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: eda0106ca978373449769fd4ded4658f, type: 3}
|
m_Script: {fileID: 11500000, guid: eda0106ca978373449769fd4ded4658f, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::Core
|
m_EditorClassIdentifier: Assembly-CSharp::Core
|
||||||
maxHealth: 100
|
|
||||||
currentHealth: 100
|
|
||||||
--- !u!33 &8496505565574430929
|
--- !u!33 &8496505565574430929
|
||||||
MeshFilter:
|
MeshFilter:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -127,3 +127,44 @@ MeshRenderer:
|
|||||||
m_SortingOrder: 0
|
m_SortingOrder: 0
|
||||||
m_MaskInteraction: 0
|
m_MaskInteraction: 0
|
||||||
m_AdditionalVertexStreams: {fileID: 0}
|
m_AdditionalVertexStreams: {fileID: 0}
|
||||||
|
--- !u!114 &5420652907638236344
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 6403733529880835406}
|
||||||
|
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: 948195259
|
||||||
|
InScenePlacedSourceGlobalObjectIdHash: 0
|
||||||
|
DeferredDespawnTick: 0
|
||||||
|
Ownership: 1
|
||||||
|
AlwaysReplicateAsRoot: 0
|
||||||
|
SynchronizeTransform: 1
|
||||||
|
ActiveSceneSynchronization: 0
|
||||||
|
SceneMigrationSynchronization: 1
|
||||||
|
SpawnWithObservers: 1
|
||||||
|
DontDestroyWithOwner: 0
|
||||||
|
AutoObjectParentSync: 1
|
||||||
|
SyncOwnerTransformWhenParented: 1
|
||||||
|
AllowOwnerToParent: 0
|
||||||
|
--- !u!114 &1005129893123536417
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 6403733529880835406}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: a4b9c07450e6c9c4b8c741b633a2702e, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::HealthComponent
|
||||||
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
|
maxHealth: 100
|
||||||
|
destroyOnDeath: 1
|
||||||
|
destroyDelay: 0
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ GameObject:
|
|||||||
- component: {fileID: 7188026176818599596}
|
- component: {fileID: 7188026176818599596}
|
||||||
- component: {fileID: 9004018437643743475}
|
- component: {fileID: 9004018437643743475}
|
||||||
- component: {fileID: 2076938100137781138}
|
- component: {fileID: 2076938100137781138}
|
||||||
|
- component: {fileID: -5264722347087192178}
|
||||||
|
- component: {fileID: 2217125893430872806}
|
||||||
m_Layer: 6
|
m_Layer: 6
|
||||||
m_Name: Enemy
|
m_Name: Enemy
|
||||||
m_TagString: Enemy
|
m_TagString: Enemy
|
||||||
@@ -211,4 +213,44 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: b2ce198f9f66bbe41a739abb07974082, type: 3}
|
m_Script: {fileID: 11500000, guid: b2ce198f9f66bbe41a739abb07974082, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::EnemyHealth
|
m_EditorClassIdentifier: Assembly-CSharp::EnemyHealth
|
||||||
maxHealth: 50
|
--- !u!114 &-5264722347087192178
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3659626783364531313}
|
||||||
|
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: 61460115
|
||||||
|
InScenePlacedSourceGlobalObjectIdHash: 0
|
||||||
|
DeferredDespawnTick: 0
|
||||||
|
Ownership: 1
|
||||||
|
AlwaysReplicateAsRoot: 0
|
||||||
|
SynchronizeTransform: 1
|
||||||
|
ActiveSceneSynchronization: 0
|
||||||
|
SceneMigrationSynchronization: 1
|
||||||
|
SpawnWithObservers: 1
|
||||||
|
DontDestroyWithOwner: 0
|
||||||
|
AutoObjectParentSync: 1
|
||||||
|
SyncOwnerTransformWhenParented: 1
|
||||||
|
AllowOwnerToParent: 0
|
||||||
|
--- !u!114 &2217125893430872806
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3659626783364531313}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: a4b9c07450e6c9c4b8c741b633a2702e, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::HealthComponent
|
||||||
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
|
maxHealth: 100
|
||||||
|
destroyOnDeath: 1
|
||||||
|
destroyDelay: 0
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ GameObject:
|
|||||||
- component: {fileID: 1378637490813329061}
|
- component: {fileID: 1378637490813329061}
|
||||||
- component: {fileID: 8775718735147868365}
|
- component: {fileID: 8775718735147868365}
|
||||||
- component: {fileID: 8186900164864976727}
|
- component: {fileID: 8186900164864976727}
|
||||||
|
- component: {fileID: 1850162772760692556}
|
||||||
|
- component: {fileID: 1697202366912309714}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: Gate
|
m_Name: Gate
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
@@ -121,8 +123,6 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: aca054ee474238545a8a396d410bf5a3, type: 3}
|
m_Script: {fileID: 11500000, guid: aca054ee474238545a8a396d410bf5a3, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::Gate
|
m_EditorClassIdentifier: Assembly-CSharp::Gate
|
||||||
maxHealth: 50
|
|
||||||
currentHealth: 50
|
|
||||||
--- !u!65 &8186900164864976727
|
--- !u!65 &8186900164864976727
|
||||||
BoxCollider:
|
BoxCollider:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -144,3 +144,44 @@ BoxCollider:
|
|||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_Size: {x: 1, y: 1, z: 1}
|
m_Size: {x: 1, y: 1, z: 1}
|
||||||
m_Center: {x: 0.5, y: 0.5, z: 0.49999997}
|
m_Center: {x: 0.5, y: 0.5, z: 0.49999997}
|
||||||
|
--- !u!114 &1850162772760692556
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3321405240327640087}
|
||||||
|
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: 2008860137
|
||||||
|
InScenePlacedSourceGlobalObjectIdHash: 0
|
||||||
|
DeferredDespawnTick: 0
|
||||||
|
Ownership: 1
|
||||||
|
AlwaysReplicateAsRoot: 0
|
||||||
|
SynchronizeTransform: 1
|
||||||
|
ActiveSceneSynchronization: 0
|
||||||
|
SceneMigrationSynchronization: 1
|
||||||
|
SpawnWithObservers: 1
|
||||||
|
DontDestroyWithOwner: 0
|
||||||
|
AutoObjectParentSync: 1
|
||||||
|
SyncOwnerTransformWhenParented: 1
|
||||||
|
AllowOwnerToParent: 0
|
||||||
|
--- !u!114 &1697202366912309714
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3321405240327640087}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: a4b9c07450e6c9c4b8c741b633a2702e, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::HealthComponent
|
||||||
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
|
maxHealth: 100
|
||||||
|
destroyOnDeath: 0
|
||||||
|
destroyDelay: 0
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ GameObject:
|
|||||||
- component: {fileID: 7528764990365051674}
|
- component: {fileID: 7528764990365051674}
|
||||||
- component: {fileID: 3421159559893464927}
|
- component: {fileID: 3421159559893464927}
|
||||||
- component: {fileID: -6050457801577222831}
|
- component: {fileID: -6050457801577222831}
|
||||||
|
- component: {fileID: 1313689911257131059}
|
||||||
m_Layer: 12
|
m_Layer: 12
|
||||||
m_Name: MineableBlock
|
m_Name: MineableBlock
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
@@ -154,9 +155,11 @@ MonoBehaviour:
|
|||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::MineableBlock
|
m_EditorClassIdentifier: Assembly-CSharp::MineableBlock
|
||||||
ShowTopMostFoldoutHeaderGroup: 1
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
maxHp: 100
|
dropItemData: {fileID: 0}
|
||||||
|
genericDropPrefab: {fileID: 0}
|
||||||
shakeDuration: 0.1
|
shakeDuration: 0.1
|
||||||
shakeMagnitude: 0.1
|
shakeMagnitude: 0.1
|
||||||
|
darkIntensity: 0.2
|
||||||
--- !u!114 &-6050457801577222831
|
--- !u!114 &-6050457801577222831
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -175,3 +178,19 @@ MonoBehaviour:
|
|||||||
precomputeOutline: 0
|
precomputeOutline: 0
|
||||||
bakeKeys: []
|
bakeKeys: []
|
||||||
bakeValues: []
|
bakeValues: []
|
||||||
|
--- !u!114 &1313689911257131059
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 989066657509100432}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: a4b9c07450e6c9c4b8c741b633a2702e, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::HealthComponent
|
||||||
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
|
maxHealth: 100
|
||||||
|
destroyOnDeath: 1
|
||||||
|
destroyDelay: 0
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ GameObject:
|
|||||||
- component: {fileID: 7528764990365051674}
|
- component: {fileID: 7528764990365051674}
|
||||||
- component: {fileID: 3421159559893464927}
|
- component: {fileID: 3421159559893464927}
|
||||||
- component: {fileID: -8077891763218709486}
|
- component: {fileID: -8077891763218709486}
|
||||||
|
- component: {fileID: 1570038371682152649}
|
||||||
m_Layer: 12
|
m_Layer: 12
|
||||||
m_Name: ResourceBlock
|
m_Name: ResourceBlock
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
@@ -128,7 +129,7 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
||||||
GlobalObjectIdHash: 822699884
|
GlobalObjectIdHash: 1191681468
|
||||||
InScenePlacedSourceGlobalObjectIdHash: 1191681468
|
InScenePlacedSourceGlobalObjectIdHash: 1191681468
|
||||||
DeferredDespawnTick: 0
|
DeferredDespawnTick: 0
|
||||||
Ownership: 1
|
Ownership: 1
|
||||||
@@ -154,11 +155,11 @@ MonoBehaviour:
|
|||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::MineableBlock
|
m_EditorClassIdentifier: Assembly-CSharp::MineableBlock
|
||||||
ShowTopMostFoldoutHeaderGroup: 1
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
maxHp: 100
|
|
||||||
dropItemData: {fileID: 11400000, guid: 953ceca9a25978549a56f0a4ff5d6a2c, type: 2}
|
dropItemData: {fileID: 11400000, guid: 953ceca9a25978549a56f0a4ff5d6a2c, type: 2}
|
||||||
genericDropPrefab: {fileID: 1253970051563370359, guid: 1d7655b1088c3ea46b8f52f6c6760047, type: 3}
|
genericDropPrefab: {fileID: 1253970051563370359, guid: 1d7655b1088c3ea46b8f52f6c6760047, type: 3}
|
||||||
shakeDuration: 0.1
|
shakeDuration: 0.1
|
||||||
shakeMagnitude: 0.1
|
shakeMagnitude: 0.1
|
||||||
|
darkIntensity: 0.2
|
||||||
--- !u!114 &-8077891763218709486
|
--- !u!114 &-8077891763218709486
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -177,3 +178,19 @@ MonoBehaviour:
|
|||||||
precomputeOutline: 0
|
precomputeOutline: 0
|
||||||
bakeKeys: []
|
bakeKeys: []
|
||||||
bakeValues: []
|
bakeValues: []
|
||||||
|
--- !u!114 &1570038371682152649
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 989066657509100432}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: a4b9c07450e6c9c4b8c741b633a2702e, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::HealthComponent
|
||||||
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
|
maxHealth: 100
|
||||||
|
destroyOnDeath: 1
|
||||||
|
destroyDelay: 0
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ MonoBehaviour:
|
|||||||
itemID: 1
|
itemID: 1
|
||||||
itemName: "\uACE1\uAD2D\uC774"
|
itemName: "\uACE1\uAD2D\uC774"
|
||||||
icon: {fileID: 1410797105229532982, guid: 206db4b680835834bb863402d6ff99f0, type: 3}
|
icon: {fileID: 1410797105229532982, guid: 206db4b680835834bb863402d6ff99f0, type: 3}
|
||||||
|
description:
|
||||||
weight: 0
|
weight: 0
|
||||||
maxStack: 1
|
maxStack: 1
|
||||||
originalBlockPrefab: {fileID: 919132149155446097, guid: ebadcbcc9e8baa1469830ca427684d65, type: 3}
|
originalBlockPrefab: {fileID: 919132149155446097, guid: ebadcbcc9e8baa1469830ca427684d65, type: 3}
|
||||||
isTool: 1
|
behavior: {fileID: 11400000, guid: 239d2d4ad6cebe042813b3d938255356, type: 2}
|
||||||
toolAction: {fileID: 11400000, guid: 677adf45880ed9e4a80b2f113fff07f8, type: 2}
|
isEquippable: 1
|
||||||
toolPrefab: {fileID: 919132149155446097, guid: ebadcbcc9e8baa1469830ca427684d65, type: 3}
|
equipmentPrefab: {fileID: 1272226527344106, guid: 346845f0154817c4297d6af7c695b28a, type: 3}
|
||||||
equipPositionOffset: {x: 0, y: 0, z: 0}
|
equipPositionOffset: {x: 0, y: 0, z: 0}
|
||||||
equipRotationOffset: {x: 0, y: 0, z: 0}
|
equipRotationOffset: {x: 0, y: 0, z: 0}
|
||||||
|
attachmentPointName: handslot.r
|
||||||
|
|||||||
23
Assets/Resources/New Mining Behavior.asset
Normal file
23
Assets/Resources/New Mining Behavior.asset
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: c1deeb9de56edff4ca77ddabf9db691a, type: 3}
|
||||||
|
m_Name: New Mining Behavior
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::MiningBehavior
|
||||||
|
behaviorName: Mining
|
||||||
|
duration: 0.5
|
||||||
|
animTrigger: Attack
|
||||||
|
animSpeed: 1
|
||||||
|
impactDelay: 0.2
|
||||||
|
canRepeat: 1
|
||||||
|
useSound: {fileID: 0}
|
||||||
|
useEffect: {fileID: 0}
|
||||||
|
damage: 50
|
||||||
8
Assets/Resources/New Mining Behavior.asset.meta
Normal file
8
Assets/Resources/New Mining Behavior.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 239d2d4ad6cebe042813b3d938255356
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -926,6 +926,22 @@ NavMeshObstacle:
|
|||||||
m_CarveOnlyStationary: 1
|
m_CarveOnlyStationary: 1
|
||||||
m_Center: {x: 0, y: 0, z: 0}
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
m_TimeToStationary: 0.5
|
m_TimeToStationary: 0.5
|
||||||
|
--- !u!114 &445606028
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 445606021}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: a4b9c07450e6c9c4b8c741b633a2702e, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::HealthComponent
|
||||||
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
|
maxHealth: 100
|
||||||
|
destroyOnDeath: 1
|
||||||
|
destroyDelay: 0
|
||||||
--- !u!1001 &497942047
|
--- !u!1001 &497942047
|
||||||
PrefabInstance:
|
PrefabInstance:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -1091,7 +1107,7 @@ MonoBehaviour:
|
|||||||
m_DisconnectTimeoutMS: 30000
|
m_DisconnectTimeoutMS: 30000
|
||||||
ConnectionData:
|
ConnectionData:
|
||||||
Address: 127.0.0.1
|
Address: 127.0.0.1
|
||||||
Port: 7793
|
Port: 7781
|
||||||
ServerListenAddress: 127.0.0.1
|
ServerListenAddress: 127.0.0.1
|
||||||
ClientBindPort: 0
|
ClientBindPort: 0
|
||||||
DebugSimulator:
|
DebugSimulator:
|
||||||
@@ -1724,6 +1740,18 @@ PrefabInstance:
|
|||||||
propertyPath: m_LocalEulerAnglesHint.z
|
propertyPath: m_LocalEulerAnglesHint.z
|
||||||
value: 0
|
value: 0
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 5420652907638236344, guid: 443aa97110814434cb36b26656f1884c, type: 3}
|
||||||
|
propertyPath: GlobalObjectIdHash
|
||||||
|
value: 3705943614
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 5420652907638236344, guid: 443aa97110814434cb36b26656f1884c, type: 3}
|
||||||
|
propertyPath: SceneMigrationSynchronization
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 5420652907638236344, guid: 443aa97110814434cb36b26656f1884c, type: 3}
|
||||||
|
propertyPath: InScenePlacedSourceGlobalObjectIdHash
|
||||||
|
value: 948195259
|
||||||
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 6403733529880835406, guid: 443aa97110814434cb36b26656f1884c, type: 3}
|
- target: {fileID: 6403733529880835406, guid: 443aa97110814434cb36b26656f1884c, type: 3}
|
||||||
propertyPath: m_Name
|
propertyPath: m_Name
|
||||||
value: Core
|
value: Core
|
||||||
@@ -1739,6 +1767,9 @@ PrefabInstance:
|
|||||||
- targetCorrespondingSourceObject: {fileID: 6403733529880835406, guid: 443aa97110814434cb36b26656f1884c, type: 3}
|
- targetCorrespondingSourceObject: {fileID: 6403733529880835406, guid: 443aa97110814434cb36b26656f1884c, type: 3}
|
||||||
insertIndex: -1
|
insertIndex: -1
|
||||||
addedObject: {fileID: 445606027}
|
addedObject: {fileID: 445606027}
|
||||||
|
- targetCorrespondingSourceObject: {fileID: 6403733529880835406, guid: 443aa97110814434cb36b26656f1884c, type: 3}
|
||||||
|
insertIndex: -1
|
||||||
|
addedObject: {fileID: 445606028}
|
||||||
m_SourcePrefab: {fileID: 100100000, guid: 443aa97110814434cb36b26656f1884c, type: 3}
|
m_SourcePrefab: {fileID: 100100000, guid: 443aa97110814434cb36b26656f1884c, type: 3}
|
||||||
--- !u!1 &1044242050
|
--- !u!1 &1044242050
|
||||||
GameObject:
|
GameObject:
|
||||||
@@ -1864,6 +1895,18 @@ PrefabInstance:
|
|||||||
propertyPath: 'm_Materials.Array.data[0]'
|
propertyPath: 'm_Materials.Array.data[0]'
|
||||||
value:
|
value:
|
||||||
objectReference: {fileID: 2100000, guid: 2425c03ff18262a4eaa45371f0fe6dcf, type: 2}
|
objectReference: {fileID: 2100000, guid: 2425c03ff18262a4eaa45371f0fe6dcf, type: 2}
|
||||||
|
- target: {fileID: 1850162772760692556, guid: 2bb7e098e271eb44a873c856dbf59c7c, type: 3}
|
||||||
|
propertyPath: GlobalObjectIdHash
|
||||||
|
value: 4230179970
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 1850162772760692556, guid: 2bb7e098e271eb44a873c856dbf59c7c, type: 3}
|
||||||
|
propertyPath: SceneMigrationSynchronization
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 1850162772760692556, guid: 2bb7e098e271eb44a873c856dbf59c7c, type: 3}
|
||||||
|
propertyPath: InScenePlacedSourceGlobalObjectIdHash
|
||||||
|
value: 2008860137
|
||||||
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 3321405240327640087, guid: 2bb7e098e271eb44a873c856dbf59c7c, type: 3}
|
- target: {fileID: 3321405240327640087, guid: 2bb7e098e271eb44a873c856dbf59c7c, type: 3}
|
||||||
propertyPath: m_Name
|
propertyPath: m_Name
|
||||||
value: Gate
|
value: Gate
|
||||||
@@ -1919,8 +1962,32 @@ PrefabInstance:
|
|||||||
m_RemovedComponents: []
|
m_RemovedComponents: []
|
||||||
m_RemovedGameObjects: []
|
m_RemovedGameObjects: []
|
||||||
m_AddedGameObjects: []
|
m_AddedGameObjects: []
|
||||||
m_AddedComponents: []
|
m_AddedComponents:
|
||||||
|
- targetCorrespondingSourceObject: {fileID: 3321405240327640087, guid: 2bb7e098e271eb44a873c856dbf59c7c, type: 3}
|
||||||
|
insertIndex: -1
|
||||||
|
addedObject: {fileID: 1075139052}
|
||||||
m_SourcePrefab: {fileID: 100100000, guid: 2bb7e098e271eb44a873c856dbf59c7c, type: 3}
|
m_SourcePrefab: {fileID: 100100000, guid: 2bb7e098e271eb44a873c856dbf59c7c, type: 3}
|
||||||
|
--- !u!1 &1075139051 stripped
|
||||||
|
GameObject:
|
||||||
|
m_CorrespondingSourceObject: {fileID: 3321405240327640087, guid: 2bb7e098e271eb44a873c856dbf59c7c, type: 3}
|
||||||
|
m_PrefabInstance: {fileID: 1075139050}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
--- !u!114 &1075139052
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1075139051}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: a4b9c07450e6c9c4b8c741b633a2702e, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::HealthComponent
|
||||||
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
|
maxHealth: 100
|
||||||
|
destroyOnDeath: 0
|
||||||
|
destroyDelay: 0
|
||||||
--- !u!1 &1095978102
|
--- !u!1 &1095978102
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -3049,6 +3116,136 @@ Camera:
|
|||||||
m_CorrespondingSourceObject: {fileID: 5650099317679730308, guid: 2b08dd32e48ef5e4aa65a6122099152e, type: 3}
|
m_CorrespondingSourceObject: {fileID: 5650099317679730308, guid: 2b08dd32e48ef5e4aa65a6122099152e, type: 3}
|
||||||
m_PrefabInstance: {fileID: 3690888448170635710}
|
m_PrefabInstance: {fileID: 3690888448170635710}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
--- !u!1 &1782529040
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1782529044}
|
||||||
|
- component: {fileID: 1782529043}
|
||||||
|
- component: {fileID: 1782529042}
|
||||||
|
- component: {fileID: 1782529041}
|
||||||
|
- component: {fileID: 1782529045}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: FogOverlay
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!64 &1782529041
|
||||||
|
MeshCollider:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1782529040}
|
||||||
|
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: 10209, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
--- !u!23 &1782529042
|
||||||
|
MeshRenderer:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1782529040}
|
||||||
|
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: 3deb4a29e039554439a14651844edec2, 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!33 &1782529043
|
||||||
|
MeshFilter:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1782529040}
|
||||||
|
m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
--- !u!4 &1782529044
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1782529040}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: -0.7071068, y: 0, z: 0, w: 0.7071068}
|
||||||
|
m_LocalPosition: {x: 0, y: 0, z: 10}
|
||||||
|
m_LocalScale: {x: 1000, y: 1, z: 1000}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: -90, y: 0, z: 0}
|
||||||
|
--- !u!114 &1782529045
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1782529040}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 4867a597119adb047ab17b69beca76be, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::FogOfWarManager
|
||||||
|
worldSize: 1000
|
||||||
|
textureSize: 512
|
||||||
|
revealRadius: 3
|
||||||
|
groundLevelY: -1
|
||||||
--- !u!1 &1842105545
|
--- !u!1 &1842105545
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -3739,3 +3936,4 @@ SceneRoots:
|
|||||||
- {fileID: 556982644}
|
- {fileID: 556982644}
|
||||||
- {fileID: 2067098344}
|
- {fileID: 2067098344}
|
||||||
- {fileID: 1384281111}
|
- {fileID: 1384281111}
|
||||||
|
- {fileID: 1782529044}
|
||||||
|
|||||||
@@ -1,33 +1,66 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using System;
|
||||||
|
|
||||||
public class EnemyHealth : MonoBehaviour, IDamageable
|
/// <summary>
|
||||||
|
/// Enemy health handler.
|
||||||
|
/// Uses HealthComponent for health management.
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(HealthComponent))]
|
||||||
|
public class EnemyHealth : MonoBehaviour
|
||||||
{
|
{
|
||||||
public float maxHealth = 50f;
|
private HealthComponent _health;
|
||||||
private float _currentHealth;
|
|
||||||
|
|
||||||
void Start()
|
/// <summary>
|
||||||
|
/// Event fired when this enemy dies.
|
||||||
|
/// </summary>
|
||||||
|
public event Action OnEnemyDeath;
|
||||||
|
|
||||||
|
void Awake()
|
||||||
{
|
{
|
||||||
_currentHealth = maxHealth;
|
_health = GetComponent<HealthComponent>();
|
||||||
|
|
||||||
|
// Subscribe to health component events
|
||||||
|
_health.OnDamaged += HandleDamaged;
|
||||||
|
_health.OnDeath += HandleDeath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void TakeDamage(float damage)
|
void OnDestroy()
|
||||||
{
|
{
|
||||||
_currentHealth -= damage;
|
if (_health != null)
|
||||||
Debug.Log($"{gameObject.name} 남은 체력: {_currentHealth}");
|
|
||||||
|
|
||||||
// 데미지 입었을 때 반짝이는 효과를 여기서 호출해도 좋습니다.
|
|
||||||
// 예: GetComponent<EnemyAttack>().StartFlash();
|
|
||||||
|
|
||||||
if (_currentHealth <= 0)
|
|
||||||
{
|
{
|
||||||
Die();
|
_health.OnDamaged -= HandleDamaged;
|
||||||
|
_health.OnDeath -= HandleDeath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Die()
|
private void HandleDamaged(DamageInfo info)
|
||||||
{
|
{
|
||||||
Debug.Log($"{gameObject.name} 사망!");
|
Debug.Log($"{gameObject.name} took {info.Amount} damage. Remaining: {_health.CurrentHealth}");
|
||||||
// 여기서 파티클 생성이나 점수 추가 등을 처리합니다.
|
}
|
||||||
|
|
||||||
|
private void HandleDeath()
|
||||||
|
{
|
||||||
|
Debug.Log($"{gameObject.name} died!");
|
||||||
|
|
||||||
|
// Fire event for external listeners (score system, spawner, etc.)
|
||||||
|
OnEnemyDeath?.Invoke();
|
||||||
|
|
||||||
|
// Destroy the enemy
|
||||||
Destroy(gameObject);
|
Destroy(gameObject);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the health component for direct access if needed.
|
||||||
|
/// </summary>
|
||||||
|
public HealthComponent Health => _health;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the current health (convenience property).
|
||||||
|
/// </summary>
|
||||||
|
public float CurrentHealth => _health != null ? _health.CurrentHealth : 0f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the max health (convenience property).
|
||||||
|
/// </summary>
|
||||||
|
public float MaxHealth => _health != null ? _health.MaxHealth : 0f;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using System;
|
|
||||||
|
|
||||||
public class Core : MonoBehaviour, IDamageable
|
/// <summary>
|
||||||
|
/// Core structure that players must defend.
|
||||||
|
/// Uses HealthComponent for health management.
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(HealthComponent))]
|
||||||
|
public class Core : MonoBehaviour
|
||||||
{
|
{
|
||||||
[SerializeField] private float maxHealth = 100f;
|
private HealthComponent _health;
|
||||||
[SerializeField] private float currentHealth = 100f;
|
|
||||||
private float CurrentHealth;
|
|
||||||
|
|
||||||
// 체력이 변경될 때 UI 등에 알리기 위한 이벤트 (Observer 패턴)
|
void Awake()
|
||||||
public static event Action<float> OnHealthChanged;
|
|
||||||
public static event Action OnCoreDestroyed;
|
|
||||||
|
|
||||||
void Awake() => currentHealth = maxHealth;
|
|
||||||
|
|
||||||
public void TakeDamage(float amount)
|
|
||||||
{
|
{
|
||||||
currentHealth -= amount;
|
_health = GetComponent<HealthComponent>();
|
||||||
OnHealthChanged?.Invoke(currentHealth / maxHealth);
|
|
||||||
|
|
||||||
if (currentHealth <= 0)
|
|
||||||
OnCoreDestroyed?.Invoke();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the health component for direct access.
|
||||||
|
/// </summary>
|
||||||
|
public HealthComponent Health => _health;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,19 +1,42 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.SceneManagement; // 씬 재시작용
|
using UnityEngine.SceneManagement;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages game state and responds to core destruction.
|
||||||
|
/// </summary>
|
||||||
public class GameManager : MonoBehaviour
|
public class GameManager : MonoBehaviour
|
||||||
{
|
{
|
||||||
private bool _isGameOver = false;
|
[SerializeField] private Core coreReference;
|
||||||
|
|
||||||
private void OnEnable()
|
private bool _isGameOver = false;
|
||||||
|
private HealthComponent _coreHealth;
|
||||||
|
|
||||||
|
private void Start()
|
||||||
{
|
{
|
||||||
// Core의 파괴 이벤트를 구독
|
// Find Core if not assigned
|
||||||
Core.OnCoreDestroyed += GameOver;
|
if (coreReference == null)
|
||||||
|
{
|
||||||
|
coreReference = FindFirstObjectByType<Core>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to core's health component
|
||||||
|
if (coreReference != null)
|
||||||
|
{
|
||||||
|
_coreHealth = coreReference.Health;
|
||||||
|
if (_coreHealth != null)
|
||||||
|
{
|
||||||
|
_coreHealth.OnDeath += GameOver;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDisable()
|
private void OnDestroy()
|
||||||
{
|
{
|
||||||
Core.OnCoreDestroyed -= GameOver;
|
// Unsubscribe to prevent memory leaks
|
||||||
|
if (_coreHealth != null)
|
||||||
|
{
|
||||||
|
_coreHealth.OnDeath -= GameOver;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GameOver()
|
private void GameOver()
|
||||||
@@ -23,14 +46,13 @@ public class GameManager : MonoBehaviour
|
|||||||
_isGameOver = true;
|
_isGameOver = true;
|
||||||
Debug.Log("Game Over! Core has been destroyed.");
|
Debug.Log("Game Over! Core has been destroyed.");
|
||||||
|
|
||||||
// 여기에 패배 UI 표시 로직 등을 넣습니다.
|
// Show defeat UI here
|
||||||
// 예: 3초 후 게임 재시작
|
// Example: restart game after 3 seconds
|
||||||
Invoke(nameof(RestartGame), 3f);
|
Invoke(nameof(RestartGame), 3f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RestartGame()
|
private void RestartGame()
|
||||||
{
|
{
|
||||||
// 현재 활성화된 씬을 다시 로드
|
|
||||||
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
|
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,51 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using System;
|
using UnityEngine.AI;
|
||||||
|
|
||||||
public class Gate : MonoBehaviour, IDamageable
|
/// <summary>
|
||||||
|
/// Gate structure that can be destroyed.
|
||||||
|
/// Uses HealthComponent for health management.
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(HealthComponent))]
|
||||||
|
public class Gate : MonoBehaviour
|
||||||
{
|
{
|
||||||
[SerializeField] private float maxHealth = 50f;
|
private HealthComponent _health;
|
||||||
[SerializeField] private float currentHealth = 50f;
|
private NavMeshObstacle _obstacle;
|
||||||
private float CurrentHealth;
|
|
||||||
|
|
||||||
// 체력이 변경될 때 UI 등에 알리기 위한 이벤트 (Observer 패턴)
|
void Awake()
|
||||||
public static event Action<float> OnHealthChanged;
|
|
||||||
public static event Action OnGateDestroyed;
|
|
||||||
|
|
||||||
void Awake() => currentHealth = maxHealth;
|
|
||||||
|
|
||||||
public void TakeDamage(float amount)
|
|
||||||
{
|
{
|
||||||
currentHealth -= amount;
|
_health = GetComponent<HealthComponent>();
|
||||||
OnHealthChanged?.Invoke(currentHealth / maxHealth);
|
_obstacle = GetComponent<NavMeshObstacle>();
|
||||||
|
|
||||||
if (currentHealth <= 0)
|
// Subscribe to health component events
|
||||||
|
_health.OnDeath += HandleDeath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDestroy()
|
||||||
|
{
|
||||||
|
if (_health != null)
|
||||||
{
|
{
|
||||||
gameObject.SetActive(false);
|
_health.OnDeath -= HandleDeath;
|
||||||
var obstacle = GetComponent<UnityEngine.AI.NavMeshObstacle>();
|
|
||||||
if(obstacle != null)
|
|
||||||
{
|
|
||||||
obstacle.carving = false;
|
|
||||||
obstacle.enabled = false;
|
|
||||||
}
|
|
||||||
OnGateDestroyed?.Invoke();
|
|
||||||
Destroy(gameObject, 0.1f);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private void HandleDeath()
|
||||||
|
{
|
||||||
|
// Disable the gate visually
|
||||||
|
gameObject.SetActive(false);
|
||||||
|
|
||||||
|
// Disable NavMesh carving so enemies can pass through
|
||||||
|
if (_obstacle != null)
|
||||||
|
{
|
||||||
|
_obstacle.carving = false;
|
||||||
|
_obstacle.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy the object after a short delay
|
||||||
|
Destroy(gameObject, 0.1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the health component for direct access.
|
||||||
|
/// </summary>
|
||||||
|
public HealthComponent Health => _health;
|
||||||
|
}
|
||||||
|
|||||||
384
Assets/Scripts/GameBase/HealthComponent.cs
Normal file
384
Assets/Scripts/GameBase/HealthComponent.cs
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
using Unity.Netcode;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reusable health component that can be attached to any entity.
|
||||||
|
/// Supports both networked (multiplayer) and local-only usage.
|
||||||
|
/// Implements IDamageable for compatibility with existing damage systems.
|
||||||
|
/// </summary>
|
||||||
|
public class HealthComponent : NetworkBehaviour, IDamageable
|
||||||
|
{
|
||||||
|
[Header("Health Settings")]
|
||||||
|
[SerializeField] private float maxHealth = 100f;
|
||||||
|
[SerializeField] private bool destroyOnDeath = false;
|
||||||
|
[SerializeField] private float destroyDelay = 0f;
|
||||||
|
|
||||||
|
// Network-synced health for multiplayer
|
||||||
|
private NetworkVariable<float> _networkHealth = new NetworkVariable<float>(
|
||||||
|
readPerm: NetworkVariableReadPermission.Everyone,
|
||||||
|
writePerm: NetworkVariableWritePermission.Server);
|
||||||
|
|
||||||
|
// Local health for non-networked entities or single-player
|
||||||
|
private float _localHealth;
|
||||||
|
|
||||||
|
// Track if we're in a networked context
|
||||||
|
private bool _isNetworked;
|
||||||
|
|
||||||
|
#region Events
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when health changes. Parameters: (currentHealth, maxHealth)
|
||||||
|
/// </summary>
|
||||||
|
public event Action<float, float> OnHealthChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when damage is taken. Parameter: DamageInfo
|
||||||
|
/// </summary>
|
||||||
|
public event Action<DamageInfo> OnDamaged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when healed. Parameter: healAmount
|
||||||
|
/// </summary>
|
||||||
|
public event Action<float> OnHealed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when health reaches zero.
|
||||||
|
/// </summary>
|
||||||
|
public event Action OnDeath;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when health is restored from zero (revived).
|
||||||
|
/// </summary>
|
||||||
|
public event Action OnRevived;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region IDamageable Implementation
|
||||||
|
|
||||||
|
public float CurrentHealth => _isNetworked ? _networkHealth.Value : _localHealth;
|
||||||
|
public float MaxHealth => maxHealth;
|
||||||
|
public bool IsAlive => CurrentHealth > 0;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Unity Lifecycle
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
_localHealth = maxHealth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnNetworkSpawn()
|
||||||
|
{
|
||||||
|
_isNetworked = true;
|
||||||
|
|
||||||
|
if (IsServer)
|
||||||
|
{
|
||||||
|
_networkHealth.Value = maxHealth;
|
||||||
|
}
|
||||||
|
|
||||||
|
_networkHealth.OnValueChanged += HandleNetworkHealthChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnNetworkDespawn()
|
||||||
|
{
|
||||||
|
_networkHealth.OnValueChanged -= HandleNetworkHealthChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Damage Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Simple damage method (backwards compatible).
|
||||||
|
/// </summary>
|
||||||
|
public void TakeDamage(float amount)
|
||||||
|
{
|
||||||
|
TakeDamage(new DamageInfo(amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enhanced damage method with full context.
|
||||||
|
/// </summary>
|
||||||
|
public void TakeDamage(DamageInfo damageInfo)
|
||||||
|
{
|
||||||
|
if (!IsAlive) return;
|
||||||
|
if (damageInfo.Amount <= 0) return;
|
||||||
|
|
||||||
|
if (_isNetworked)
|
||||||
|
{
|
||||||
|
// In networked mode, request damage through server
|
||||||
|
ApplyDamageServerRpc(damageInfo.Amount, (int)damageInfo.Type);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Local mode - apply directly
|
||||||
|
ApplyDamageLocal(damageInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||||
|
private void ApplyDamageServerRpc(float amount, int damageType)
|
||||||
|
{
|
||||||
|
if (!IsAlive) return;
|
||||||
|
|
||||||
|
float newHealth = Mathf.Max(0, _networkHealth.Value - amount);
|
||||||
|
_networkHealth.Value = newHealth;
|
||||||
|
|
||||||
|
// Notify all clients about the damage
|
||||||
|
NotifyDamageClientRpc(amount, damageType);
|
||||||
|
|
||||||
|
if (newHealth <= 0)
|
||||||
|
{
|
||||||
|
HandleDeathServer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ClientRpc]
|
||||||
|
private void NotifyDamageClientRpc(float amount, int damageType)
|
||||||
|
{
|
||||||
|
OnDamaged?.Invoke(new DamageInfo(amount, (DamageType)damageType));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyDamageLocal(DamageInfo info)
|
||||||
|
{
|
||||||
|
float previousHealth = _localHealth;
|
||||||
|
_localHealth = Mathf.Max(0, _localHealth - info.Amount);
|
||||||
|
|
||||||
|
OnHealthChanged?.Invoke(_localHealth, maxHealth);
|
||||||
|
OnDamaged?.Invoke(info);
|
||||||
|
|
||||||
|
if (_localHealth <= 0 && previousHealth > 0)
|
||||||
|
{
|
||||||
|
HandleDeathLocal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Healing Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Heal the entity by a specified amount.
|
||||||
|
/// </summary>
|
||||||
|
public void Heal(float amount)
|
||||||
|
{
|
||||||
|
if (amount <= 0) return;
|
||||||
|
|
||||||
|
if (_isNetworked)
|
||||||
|
{
|
||||||
|
HealServerRpc(amount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HealLocal(amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||||
|
private void HealServerRpc(float amount)
|
||||||
|
{
|
||||||
|
bool wasAlive = _networkHealth.Value > 0;
|
||||||
|
float newHealth = Mathf.Min(maxHealth, _networkHealth.Value + amount);
|
||||||
|
_networkHealth.Value = newHealth;
|
||||||
|
|
||||||
|
NotifyHealClientRpc(amount, !wasAlive && newHealth > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ClientRpc]
|
||||||
|
private void NotifyHealClientRpc(float amount, bool revived)
|
||||||
|
{
|
||||||
|
OnHealed?.Invoke(amount);
|
||||||
|
if (revived)
|
||||||
|
{
|
||||||
|
OnRevived?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HealLocal(float amount)
|
||||||
|
{
|
||||||
|
bool wasAlive = _localHealth > 0;
|
||||||
|
_localHealth = Mathf.Min(maxHealth, _localHealth + amount);
|
||||||
|
|
||||||
|
OnHealthChanged?.Invoke(_localHealth, maxHealth);
|
||||||
|
OnHealed?.Invoke(amount);
|
||||||
|
|
||||||
|
if (!wasAlive && _localHealth > 0)
|
||||||
|
{
|
||||||
|
OnRevived?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fully restore health to maximum.
|
||||||
|
/// </summary>
|
||||||
|
public void HealToFull()
|
||||||
|
{
|
||||||
|
Heal(maxHealth - CurrentHealth);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Health Modification
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the maximum health value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="newMax">New maximum health</param>
|
||||||
|
/// <param name="healToMax">If true, also sets current health to the new max</param>
|
||||||
|
public void SetMaxHealth(float newMax, bool healToMax = false)
|
||||||
|
{
|
||||||
|
maxHealth = Mathf.Max(1, newMax);
|
||||||
|
|
||||||
|
if (healToMax)
|
||||||
|
{
|
||||||
|
if (_isNetworked && IsServer)
|
||||||
|
{
|
||||||
|
_networkHealth.Value = maxHealth;
|
||||||
|
}
|
||||||
|
else if (!_isNetworked)
|
||||||
|
{
|
||||||
|
_localHealth = maxHealth;
|
||||||
|
OnHealthChanged?.Invoke(_localHealth, maxHealth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Clamp current health to new max
|
||||||
|
if (_isNetworked && IsServer)
|
||||||
|
{
|
||||||
|
_networkHealth.Value = Mathf.Min(_networkHealth.Value, maxHealth);
|
||||||
|
}
|
||||||
|
else if (!_isNetworked)
|
||||||
|
{
|
||||||
|
_localHealth = Mathf.Min(_localHealth, maxHealth);
|
||||||
|
OnHealthChanged?.Invoke(_localHealth, maxHealth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Directly set the current health (use with caution).
|
||||||
|
/// </summary>
|
||||||
|
public void SetHealth(float health)
|
||||||
|
{
|
||||||
|
float newHealth = Mathf.Clamp(health, 0, maxHealth);
|
||||||
|
|
||||||
|
if (_isNetworked && IsServer)
|
||||||
|
{
|
||||||
|
_networkHealth.Value = newHealth;
|
||||||
|
}
|
||||||
|
else if (!_isNetworked)
|
||||||
|
{
|
||||||
|
_localHealth = newHealth;
|
||||||
|
OnHealthChanged?.Invoke(_localHealth, maxHealth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Death Handling
|
||||||
|
|
||||||
|
private void HandleDeathServer()
|
||||||
|
{
|
||||||
|
NotifyDeathClientRpc();
|
||||||
|
|
||||||
|
if (destroyOnDeath)
|
||||||
|
{
|
||||||
|
if (destroyDelay > 0)
|
||||||
|
{
|
||||||
|
// Schedule despawn
|
||||||
|
Invoke(nameof(DespawnObject), destroyDelay);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DespawnObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DespawnObject()
|
||||||
|
{
|
||||||
|
if (TryGetComponent<NetworkObject>(out var netObj))
|
||||||
|
{
|
||||||
|
netObj.Despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ClientRpc]
|
||||||
|
private void NotifyDeathClientRpc()
|
||||||
|
{
|
||||||
|
OnDeath?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleDeathLocal()
|
||||||
|
{
|
||||||
|
OnDeath?.Invoke();
|
||||||
|
|
||||||
|
if (destroyOnDeath)
|
||||||
|
{
|
||||||
|
if (destroyDelay > 0)
|
||||||
|
{
|
||||||
|
Destroy(gameObject, destroyDelay);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Destroy(gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Network Health Change Handler
|
||||||
|
|
||||||
|
private void HandleNetworkHealthChanged(float previousValue, float newValue)
|
||||||
|
{
|
||||||
|
OnHealthChanged?.Invoke(newValue, maxHealth);
|
||||||
|
|
||||||
|
// Check for death on clients (server handles it in ApplyDamageServerRpc)
|
||||||
|
if (previousValue > 0 && newValue <= 0 && !IsServer)
|
||||||
|
{
|
||||||
|
// Death event already sent via ClientRpc, just update local state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Utility Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get health as a normalized value (0-1).
|
||||||
|
/// </summary>
|
||||||
|
public float GetHealthNormalized()
|
||||||
|
{
|
||||||
|
return maxHealth > 0 ? CurrentHealth / maxHealth : 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if health is below a certain percentage.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsHealthBelow(float percentage)
|
||||||
|
{
|
||||||
|
return GetHealthNormalized() < percentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if at full health.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsAtFullHealth()
|
||||||
|
{
|
||||||
|
return CurrentHealth >= maxHealth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kill the entity instantly.
|
||||||
|
/// </summary>
|
||||||
|
public void Kill()
|
||||||
|
{
|
||||||
|
TakeDamage(new DamageInfo(CurrentHealth + 1, DamageType.True));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
2
Assets/Scripts/GameBase/HealthComponent.cs.meta
Normal file
2
Assets/Scripts/GameBase/HealthComponent.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a4b9c07450e6c9c4b8c741b633a2702e
|
||||||
@@ -1,5 +1,141 @@
|
|||||||
// IDamageable.cs
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Types of damage that can be dealt.
|
||||||
|
/// Used for damage resistance/vulnerability systems.
|
||||||
|
/// </summary>
|
||||||
|
public enum DamageType
|
||||||
|
{
|
||||||
|
Physical,
|
||||||
|
Magical,
|
||||||
|
Mining,
|
||||||
|
Environmental,
|
||||||
|
True // Ignores resistances
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains all information about a damage event.
|
||||||
|
/// </summary>
|
||||||
|
[System.Serializable]
|
||||||
|
public struct DamageInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Amount of damage to deal.
|
||||||
|
/// </summary>
|
||||||
|
public float Amount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Type of damage being dealt.
|
||||||
|
/// </summary>
|
||||||
|
public DamageType Type;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The GameObject that caused the damage (can be null).
|
||||||
|
/// </summary>
|
||||||
|
public GameObject Source;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// World position where the damage was applied.
|
||||||
|
/// </summary>
|
||||||
|
public Vector3 HitPoint;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Direction the damage came from (for knockback, effects, etc.).
|
||||||
|
/// </summary>
|
||||||
|
public Vector3 HitDirection;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a simple damage info with just an amount.
|
||||||
|
/// </summary>
|
||||||
|
public DamageInfo(float amount)
|
||||||
|
{
|
||||||
|
Amount = amount;
|
||||||
|
Type = DamageType.Physical;
|
||||||
|
Source = null;
|
||||||
|
HitPoint = Vector3.zero;
|
||||||
|
HitDirection = Vector3.zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create damage info with amount and type.
|
||||||
|
/// </summary>
|
||||||
|
public DamageInfo(float amount, DamageType type)
|
||||||
|
{
|
||||||
|
Amount = amount;
|
||||||
|
Type = type;
|
||||||
|
Source = null;
|
||||||
|
HitPoint = Vector3.zero;
|
||||||
|
HitDirection = Vector3.zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create full damage info with all parameters.
|
||||||
|
/// </summary>
|
||||||
|
public DamageInfo(float amount, DamageType type, GameObject source,
|
||||||
|
Vector3 hitPoint = default, Vector3 hitDirection = default)
|
||||||
|
{
|
||||||
|
Amount = amount;
|
||||||
|
Type = type;
|
||||||
|
Source = source;
|
||||||
|
HitPoint = hitPoint;
|
||||||
|
HitDirection = hitDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create mining damage.
|
||||||
|
/// </summary>
|
||||||
|
public static DamageInfo Mining(float amount, GameObject source = null)
|
||||||
|
{
|
||||||
|
return new DamageInfo(amount, DamageType.Mining, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create physical damage with source and direction.
|
||||||
|
/// </summary>
|
||||||
|
public static DamageInfo Physical(float amount, GameObject source, Vector3 hitPoint, Vector3 direction)
|
||||||
|
{
|
||||||
|
return new DamageInfo(amount, DamageType.Physical, source, hitPoint, direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for any object that can take damage.
|
||||||
|
/// </summary>
|
||||||
public interface IDamageable
|
public interface IDamageable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Simple damage method for backwards compatibility.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="amount">Amount of damage to deal</param>
|
||||||
void TakeDamage(float amount);
|
void TakeDamage(float amount);
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enhanced damage method with full context.
|
||||||
|
/// Default implementation calls the simple TakeDamage for backwards compatibility.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="damageInfo">Full damage information</param>
|
||||||
|
void TakeDamage(DamageInfo damageInfo)
|
||||||
|
{
|
||||||
|
TakeDamage(damageInfo.Amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current health value.
|
||||||
|
/// </summary>
|
||||||
|
float CurrentHealth { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum health value.
|
||||||
|
/// </summary>
|
||||||
|
float MaxHealth { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this object is still alive.
|
||||||
|
/// </summary>
|
||||||
|
bool IsAlive { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Health as a percentage (0-1).
|
||||||
|
/// </summary>
|
||||||
|
float HealthPercent => MaxHealth > 0 ? CurrentHealth / MaxHealth : 0f;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,23 +1,118 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ScriptableObject defining item properties.
|
||||||
|
/// Implements IUsableItem and IEquippableItem for extensibility.
|
||||||
|
/// </summary>
|
||||||
[CreateAssetMenu(fileName = "New Item", menuName = "Inventory/Item")]
|
[CreateAssetMenu(fileName = "New Item", menuName = "Inventory/Item")]
|
||||||
public class ItemData : ScriptableObject
|
public class ItemData : ScriptableObject, IUsableItem, IEquippableItem
|
||||||
{
|
{
|
||||||
|
[Header("Basic Info")]
|
||||||
public int itemID;
|
public int itemID;
|
||||||
public string itemName;
|
public string itemName;
|
||||||
public Sprite icon;
|
public Sprite icon;
|
||||||
public float weight; // 아이템 개당 무게
|
[TextArea] public string description;
|
||||||
public int maxStack = 99; // 최대 중첩 개수
|
|
||||||
|
[Header("Stack & Weight")]
|
||||||
|
public float weight = 1f;
|
||||||
|
public int maxStack = 99;
|
||||||
|
|
||||||
[Header("Visual Source")]
|
[Header("Visual Source")]
|
||||||
public GameObject originalBlockPrefab; // 이제 이것만 있으면 됩니다!
|
[Tooltip("Original prefab for dropped item visuals")]
|
||||||
|
public GameObject originalBlockPrefab;
|
||||||
|
|
||||||
[Header("Tool Settings")]
|
[Header("Item Behavior")]
|
||||||
public bool isTool; // 도구 여부
|
[Tooltip("Defines what happens when the item is used")]
|
||||||
public PlayerActionData toolAction; // 이 도구를 들었을 때 나갈 액션 (예: MiningActionData)
|
public ItemBehavior behavior;
|
||||||
|
|
||||||
[Header("Visual Settings")]
|
[Header("Equipment Settings")]
|
||||||
public GameObject toolPrefab; // 캐릭터 손에 스폰될 3D 프리팹
|
[Tooltip("Whether this item can be equipped (shows in hand)")]
|
||||||
public Vector3 equipPositionOffset; // 손 위치 미세 조정
|
public bool isEquippable;
|
||||||
public Vector3 equipRotationOffset; // 손 회전 미세 조정
|
[Tooltip("Prefab spawned in player's hand when equipped")]
|
||||||
}
|
public GameObject equipmentPrefab;
|
||||||
|
public Vector3 equipPositionOffset;
|
||||||
|
public Vector3 equipRotationOffset;
|
||||||
|
[Tooltip("Name of the transform to attach equipment to")]
|
||||||
|
public string attachmentPointName = "ToolAnchor";
|
||||||
|
|
||||||
|
#region IUsableItem Implementation
|
||||||
|
|
||||||
|
public bool IsConsumable => behavior != null && behavior.IsConsumable;
|
||||||
|
|
||||||
|
public bool CanUse(GameObject user, GameObject target)
|
||||||
|
{
|
||||||
|
return behavior != null && behavior.CanUse(user, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Use(GameObject user, GameObject target)
|
||||||
|
{
|
||||||
|
if (behavior != null)
|
||||||
|
{
|
||||||
|
behavior.Use(user, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActionDescriptor GetUseAction()
|
||||||
|
{
|
||||||
|
return behavior?.GetActionDescriptor();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region IEquippableItem Implementation
|
||||||
|
|
||||||
|
public GameObject GetEquipmentPrefab() => equipmentPrefab;
|
||||||
|
public Vector3 GetPositionOffset() => equipPositionOffset;
|
||||||
|
public Vector3 GetRotationOffset() => equipRotationOffset;
|
||||||
|
public string GetAttachmentPointName() => attachmentPointName;
|
||||||
|
|
||||||
|
public Transform FindAttachmentPoint(GameObject user)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(attachmentPointName))
|
||||||
|
return user.transform;
|
||||||
|
|
||||||
|
var transforms = user.GetComponentsInChildren<Transform>();
|
||||||
|
foreach (var t in transforms)
|
||||||
|
{
|
||||||
|
if (t.name == attachmentPointName)
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Utility Properties
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if this item has any usable behavior.
|
||||||
|
/// </summary>
|
||||||
|
public bool HasBehavior => behavior != null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if this item can be equipped.
|
||||||
|
/// </summary>
|
||||||
|
public bool CanBeEquipped => isEquippable && equipmentPrefab != null;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Utility Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get display name for UI.
|
||||||
|
/// </summary>
|
||||||
|
public string GetDisplayName() => string.IsNullOrEmpty(itemName) ? name : itemName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get description for UI.
|
||||||
|
/// </summary>
|
||||||
|
public string GetDescription() => description;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculate total weight for a stack.
|
||||||
|
/// </summary>
|
||||||
|
public float GetStackWeight(int count) => weight * count;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,172 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Types of interactions available in the game.
|
||||||
|
/// </summary>
|
||||||
|
public enum InteractionType
|
||||||
|
{
|
||||||
|
Generic,
|
||||||
|
Pickup,
|
||||||
|
Use,
|
||||||
|
Talk,
|
||||||
|
Enter,
|
||||||
|
Build,
|
||||||
|
Open,
|
||||||
|
Activate
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains preview information about an interaction for UI display.
|
||||||
|
/// </summary>
|
||||||
|
[System.Serializable]
|
||||||
|
public struct InteractionPreview
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The action verb (e.g., "Pick up", "Enter", "Use").
|
||||||
|
/// </summary>
|
||||||
|
public string ActionVerb;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the target object (e.g., "Iron Ore", "Tunnel").
|
||||||
|
/// </summary>
|
||||||
|
public string TargetName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional icon to display in UI.
|
||||||
|
/// </summary>
|
||||||
|
public Sprite Icon;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The type of interaction.
|
||||||
|
/// </summary>
|
||||||
|
public InteractionType Type;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this interaction requires holding the button.
|
||||||
|
/// </summary>
|
||||||
|
public bool RequiresHold;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Duration to hold if RequiresHold is true.
|
||||||
|
/// </summary>
|
||||||
|
public float HoldDuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Input hint to display (e.g., "[F]", "[E]").
|
||||||
|
/// </summary>
|
||||||
|
public string InputHint;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a simple interaction preview.
|
||||||
|
/// </summary>
|
||||||
|
public static InteractionPreview Simple(string actionVerb, string targetName,
|
||||||
|
InteractionType type = InteractionType.Generic)
|
||||||
|
{
|
||||||
|
return new InteractionPreview
|
||||||
|
{
|
||||||
|
ActionVerb = actionVerb,
|
||||||
|
TargetName = targetName,
|
||||||
|
Type = type,
|
||||||
|
RequiresHold = false,
|
||||||
|
HoldDuration = 0f,
|
||||||
|
InputHint = "[F]"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a pickup interaction preview.
|
||||||
|
/// </summary>
|
||||||
|
public static InteractionPreview Pickup(string itemName, Sprite icon = null)
|
||||||
|
{
|
||||||
|
return new InteractionPreview
|
||||||
|
{
|
||||||
|
ActionVerb = "Pick up",
|
||||||
|
TargetName = itemName,
|
||||||
|
Icon = icon,
|
||||||
|
Type = InteractionType.Pickup,
|
||||||
|
RequiresHold = false,
|
||||||
|
InputHint = "[F]"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create an enter/use interaction preview.
|
||||||
|
/// </summary>
|
||||||
|
public static InteractionPreview Enter(string targetName)
|
||||||
|
{
|
||||||
|
return new InteractionPreview
|
||||||
|
{
|
||||||
|
ActionVerb = "Enter",
|
||||||
|
TargetName = targetName,
|
||||||
|
Type = InteractionType.Enter,
|
||||||
|
RequiresHold = false,
|
||||||
|
InputHint = "[F]"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a hold-to-interact preview.
|
||||||
|
/// </summary>
|
||||||
|
public static InteractionPreview Hold(string actionVerb, string targetName, float duration,
|
||||||
|
InteractionType type = InteractionType.Use)
|
||||||
|
{
|
||||||
|
return new InteractionPreview
|
||||||
|
{
|
||||||
|
ActionVerb = actionVerb,
|
||||||
|
TargetName = targetName,
|
||||||
|
Type = type,
|
||||||
|
RequiresHold = true,
|
||||||
|
HoldDuration = duration,
|
||||||
|
InputHint = "[Hold F]"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the full display string (e.g., "[F] Pick up Iron Ore").
|
||||||
|
/// </summary>
|
||||||
|
public string GetDisplayString()
|
||||||
|
{
|
||||||
|
return $"{InputHint} {ActionVerb} {TargetName}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for objects that can be interacted with by the player.
|
||||||
|
/// </summary>
|
||||||
public interface IInteractable
|
public interface IInteractable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Perform the interaction.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="interactor">The GameObject performing the interaction (usually the player)</param>
|
||||||
void Interact(GameObject interactor);
|
void Interact(GameObject interactor);
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get preview information about this interaction for UI display.
|
||||||
|
/// Default implementation returns a generic preview.
|
||||||
|
/// </summary>
|
||||||
|
InteractionPreview GetInteractionPreview()
|
||||||
|
{
|
||||||
|
return InteractionPreview.Simple("Interact", "Object");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if this object can currently be interacted with.
|
||||||
|
/// Default implementation always returns true.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="interactor">The GameObject attempting to interact</param>
|
||||||
|
/// <returns>True if interaction is possible</returns>
|
||||||
|
bool CanInteract(GameObject interactor)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the world position of this interactable for distance calculations.
|
||||||
|
/// Default implementation returns zero vector (override in implementation).
|
||||||
|
/// </summary>
|
||||||
|
Vector3 GetInteractionPoint()
|
||||||
|
{
|
||||||
|
return Vector3.zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
8
Assets/Scripts/Items.meta
Normal file
8
Assets/Scripts/Items.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: afa213ad718d66646ba597d72845441d
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
63
Assets/Scripts/Items/IUsableItem.cs
Normal file
63
Assets/Scripts/Items/IUsableItem.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for items that can be used/activated by the player.
|
||||||
|
/// </summary>
|
||||||
|
public interface IUsableItem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the item can be used.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The GameObject using the item (player)</param>
|
||||||
|
/// <param name="target">Optional target for the use action</param>
|
||||||
|
/// <returns>True if the item can be used</returns>
|
||||||
|
bool CanUse(GameObject user, GameObject target);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Use the item.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The GameObject using the item (player)</param>
|
||||||
|
/// <param name="target">Optional target for the use action</param>
|
||||||
|
void Use(GameObject user, GameObject target);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the action descriptor for using this item.
|
||||||
|
/// </summary>
|
||||||
|
ActionDescriptor GetUseAction();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether using this item consumes it (reduces stack count).
|
||||||
|
/// </summary>
|
||||||
|
bool IsConsumable { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for items that can be equipped on the player.
|
||||||
|
/// </summary>
|
||||||
|
public interface IEquippableItem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get the equipment prefab to spawn.
|
||||||
|
/// </summary>
|
||||||
|
GameObject GetEquipmentPrefab();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get position offset for equipment placement.
|
||||||
|
/// </summary>
|
||||||
|
Vector3 GetPositionOffset();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get rotation offset for equipment placement.
|
||||||
|
/// </summary>
|
||||||
|
Vector3 GetRotationOffset();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the name of the attachment point on the character.
|
||||||
|
/// </summary>
|
||||||
|
string GetAttachmentPointName();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Find the attachment point transform on a user.
|
||||||
|
/// </summary>
|
||||||
|
Transform FindAttachmentPoint(GameObject user);
|
||||||
|
}
|
||||||
2
Assets/Scripts/Items/IUsableItem.cs.meta
Normal file
2
Assets/Scripts/Items/IUsableItem.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 686008e086e9d2247b33d5828e0efa5f
|
||||||
67
Assets/Scripts/Items/ItemBehavior.cs
Normal file
67
Assets/Scripts/Items/ItemBehavior.cs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for item behaviors. Allows different items to have
|
||||||
|
/// completely different use effects without modifying ItemData.
|
||||||
|
/// This is the Strategy pattern for item actions.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class ItemBehavior : ScriptableObject
|
||||||
|
{
|
||||||
|
[Header("Basic Settings")]
|
||||||
|
[SerializeField] protected string behaviorName = "Use";
|
||||||
|
[SerializeField] protected float duration = 0.5f;
|
||||||
|
[SerializeField] protected string animTrigger = "Use";
|
||||||
|
[SerializeField] protected float animSpeed = 1f;
|
||||||
|
[SerializeField] protected float impactDelay = 0.2f;
|
||||||
|
[SerializeField] protected bool canRepeat = false;
|
||||||
|
|
||||||
|
[Header("Effects")]
|
||||||
|
[SerializeField] protected AudioClip useSound;
|
||||||
|
[SerializeField] protected GameObject useEffect;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this behavior consumes the item when used.
|
||||||
|
/// </summary>
|
||||||
|
public virtual bool IsConsumable => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if this behavior can be used with the given user and target.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The player/entity using the item</param>
|
||||||
|
/// <param name="target">Optional target of the use</param>
|
||||||
|
/// <returns>True if the behavior can be executed</returns>
|
||||||
|
public abstract bool CanUse(GameObject user, GameObject target);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute the behavior's effect.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The player/entity using the item</param>
|
||||||
|
/// <param name="target">Optional target of the use</param>
|
||||||
|
public abstract void Use(GameObject user, GameObject target);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the action descriptor for this behavior.
|
||||||
|
/// </summary>
|
||||||
|
public virtual ActionDescriptor GetActionDescriptor()
|
||||||
|
{
|
||||||
|
return new ActionDescriptor
|
||||||
|
{
|
||||||
|
ActionName = behaviorName,
|
||||||
|
Duration = duration,
|
||||||
|
AnimTrigger = animTrigger,
|
||||||
|
AnimSpeed = animSpeed,
|
||||||
|
ImpactDelay = impactDelay,
|
||||||
|
CanRepeat = canRepeat,
|
||||||
|
SoundEffect = useSound,
|
||||||
|
ParticleEffect = useEffect
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a description of why the behavior cannot be used.
|
||||||
|
/// </summary>
|
||||||
|
public virtual string GetBlockedReason(GameObject user, GameObject target)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Scripts/Items/ItemBehavior.cs.meta
Normal file
2
Assets/Scripts/Items/ItemBehavior.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 73a8d5e271a199f4598ae20f5b20a466
|
||||||
@@ -1,98 +1,131 @@
|
|||||||
using NUnit.Framework.Interfaces;
|
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using Unity.Netcode;
|
using Unity.Netcode;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
public class MineableBlock : NetworkBehaviour
|
/// <summary>
|
||||||
|
/// A block that can be mined by players.
|
||||||
|
/// Uses HealthComponent for health management and implements IDamageable.
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(HealthComponent))]
|
||||||
|
public class MineableBlock : NetworkBehaviour, IDamageable
|
||||||
{
|
{
|
||||||
[Header("Block Stats")]
|
[Header("Drop Settings")]
|
||||||
[SerializeField] private int maxHp = 100;
|
|
||||||
// [동기화] 모든 플레이어가 동일한 블록 체력을 보게 함
|
|
||||||
private NetworkVariable<int> _currentHp = new NetworkVariable<int>();
|
|
||||||
|
|
||||||
[SerializeField] private ItemData dropItemData;
|
[SerializeField] private ItemData dropItemData;
|
||||||
[SerializeField] private GameObject genericDropPrefab; // 여기에 위에서 만든 'GenericDroppedItem' 프리팹을 넣으세요.
|
[SerializeField] private GameObject genericDropPrefab;
|
||||||
|
|
||||||
[Header("Visuals")]
|
[Header("Visuals")]
|
||||||
private Outline _outline;
|
private Outline _outline;
|
||||||
private Vector3 _originalPos;
|
private Vector3 _originalPos;
|
||||||
|
|
||||||
[Header("Shake Settings")]
|
[Header("Shake Settings")]
|
||||||
[SerializeField] private float shakeDuration = 0.15f; // 흔들리는 시간
|
[SerializeField] private float shakeDuration = 0.15f;
|
||||||
[SerializeField] private float shakeMagnitude = 0.1f; // 흔들리는 강도
|
[SerializeField] private float shakeMagnitude = 0.1f;
|
||||||
private Coroutine _shakeCoroutine;
|
private Coroutine _shakeCoroutine;
|
||||||
|
|
||||||
private Color _originalColor; // 본래의 색상을 저장할 변수
|
private Color _originalColor;
|
||||||
private float _lastVisibleTime;
|
private float _lastVisibleTime;
|
||||||
private const float VisibilityThreshold = 0.25f;
|
private const float VisibilityThreshold = 0.25f;
|
||||||
|
|
||||||
[Header("Fog Settings")]
|
[Header("Fog Settings")]
|
||||||
[Range(0f, 1f)]
|
[Range(0f, 1f)]
|
||||||
[SerializeField] private float darkIntensity = 0.2f; // 안개 속에서 얼마나 어두워질지 (0: 완전 검정, 1: 원본)
|
[SerializeField] private float darkIntensity = 0.2f;
|
||||||
private MaterialPropertyBlock _propBlock;
|
private MaterialPropertyBlock _propBlock;
|
||||||
|
|
||||||
private NetworkVariable<bool> isDiscovered = new NetworkVariable<bool>(false);
|
private NetworkVariable<bool> isDiscovered = new NetworkVariable<bool>(false);
|
||||||
private MeshRenderer _renderer;
|
private MeshRenderer _renderer;
|
||||||
|
private HealthComponent _health;
|
||||||
|
|
||||||
|
#region IDamageable Implementation
|
||||||
|
|
||||||
|
public float CurrentHealth => _health != null ? _health.CurrentHealth : 0f;
|
||||||
|
public float MaxHealth => _health != null ? _health.MaxHealth : 0f;
|
||||||
|
public bool IsAlive => _health != null && _health.IsAlive;
|
||||||
|
|
||||||
|
public void TakeDamage(float amount)
|
||||||
|
{
|
||||||
|
TakeDamage(new DamageInfo(amount, DamageType.Mining));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TakeDamage(DamageInfo damageInfo)
|
||||||
|
{
|
||||||
|
if (_health != null)
|
||||||
|
{
|
||||||
|
_health.TakeDamage(damageInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
void Awake()
|
void Awake()
|
||||||
{
|
{
|
||||||
|
_health = GetComponent<HealthComponent>();
|
||||||
_renderer = GetComponentInChildren<MeshRenderer>();
|
_renderer = GetComponentInChildren<MeshRenderer>();
|
||||||
_propBlock = new MaterialPropertyBlock();
|
_propBlock = new MaterialPropertyBlock();
|
||||||
|
|
||||||
// 시작 시에는 보이지 않게 설정
|
// Start hidden
|
||||||
if (_renderer != null) _renderer.enabled = false;
|
if (_renderer != null) _renderer.enabled = false;
|
||||||
|
|
||||||
_originalColor = _renderer.sharedMaterial.HasProperty("_BaseColor")
|
if (_renderer != null && _renderer.sharedMaterial != null)
|
||||||
|
{
|
||||||
|
_originalColor = _renderer.sharedMaterial.HasProperty("_BaseColor")
|
||||||
? _renderer.sharedMaterial.GetColor("_BaseColor")
|
? _renderer.sharedMaterial.GetColor("_BaseColor")
|
||||||
: _renderer.sharedMaterial.GetColor("_Color");
|
: _renderer.sharedMaterial.GetColor("_Color");
|
||||||
|
}
|
||||||
|
|
||||||
_renderer.enabled = false;
|
// Find outline component
|
||||||
|
|
||||||
// 해당 오브젝트 혹은 자식에게서 Outline 컴포넌트를 찾습니다.
|
|
||||||
_outline = GetComponentInChildren<Outline>();
|
_outline = GetComponentInChildren<Outline>();
|
||||||
_originalPos = transform.localPosition; // 로컬 위치 저장
|
_originalPos = transform.localPosition;
|
||||||
|
|
||||||
if (_outline != null)
|
if (_outline != null)
|
||||||
{
|
{
|
||||||
// 게임 시작 시 하이라이트는 꺼둡니다.
|
|
||||||
_outline.enabled = false;
|
_outline.enabled = false;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
// Subscribe to health events
|
||||||
|
if (_health != null)
|
||||||
{
|
{
|
||||||
Debug.LogWarning($"{gameObject.name}: QuickOutline 에셋의 Outline 컴포넌트를 찾을 수 없습니다.");
|
_health.OnDamaged += HandleDamaged;
|
||||||
|
_health.OnDeath += HandleDeath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDestroy()
|
||||||
|
{
|
||||||
|
if (_health != null)
|
||||||
|
{
|
||||||
|
_health.OnDamaged -= HandleDamaged;
|
||||||
|
_health.OnDeath -= HandleDeath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnNetworkSpawn()
|
public override void OnNetworkSpawn()
|
||||||
{
|
{
|
||||||
if (IsServer)
|
// Update visuals when discovered state syncs
|
||||||
{
|
|
||||||
_currentHp.Value = maxHp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 데이터가 동기화될 때 비주얼 업데이트
|
|
||||||
UpdateVisuals(isDiscovered.Value);
|
UpdateVisuals(isDiscovered.Value);
|
||||||
|
|
||||||
isDiscovered.OnValueChanged += (oldVal, newVal) => {
|
isDiscovered.OnValueChanged += (oldVal, newVal) =>
|
||||||
|
{
|
||||||
if (newVal) UpdateState();
|
if (newVal) UpdateState();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void Update()
|
void Update()
|
||||||
{
|
{
|
||||||
// 1. 이미 발견된 블록인지는 서버 변수(isDiscovered)로 확인
|
// Check if block should be discovered based on player distance
|
||||||
// 2. 현재 내 위치가 안개에서 벗어났는지 확인 (매우 단순화된 로직)
|
if (!isDiscovered.Value && NetworkManager.Singleton != null &&
|
||||||
if (!isDiscovered.Value)
|
NetworkManager.Singleton.LocalClient != null &&
|
||||||
|
NetworkManager.Singleton.LocalClient.PlayerObject != null)
|
||||||
{
|
{
|
||||||
float dist = Vector3.Distance(transform.position, NetworkManager.Singleton.LocalClient.PlayerObject.transform.position);
|
float dist = Vector3.Distance(transform.position,
|
||||||
if (dist < FogOfWarManager.Instance.revealRadius)
|
NetworkManager.Singleton.LocalClient.PlayerObject.transform.position);
|
||||||
|
|
||||||
|
if (FogOfWarManager.Instance != null && dist < FogOfWarManager.Instance.revealRadius)
|
||||||
{
|
{
|
||||||
// 서버에 "나 발견됐어!"라고 보고
|
|
||||||
RequestRevealServerRpc();
|
RequestRevealServerRpc();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 비주얼 업데이트: 발견된 적이 있을 때만 렌더러를 켬
|
// Update renderer visibility
|
||||||
if (_renderer != null)
|
if (_renderer != null)
|
||||||
{
|
{
|
||||||
_renderer.enabled = isDiscovered.Value;
|
_renderer.enabled = isDiscovered.Value;
|
||||||
@@ -109,64 +142,64 @@ public class MineableBlock : NetworkBehaviour
|
|||||||
|
|
||||||
private void UpdateState()
|
private void UpdateState()
|
||||||
{
|
{
|
||||||
if (_renderer == null) return;
|
if (_renderer == null || !_renderer.enabled) return;
|
||||||
|
|
||||||
// 2. 현재 시야 안에 있는지 판단합니다.
|
|
||||||
bool isCurrentlyVisible = (Time.time - _lastVisibleTime) < VisibilityThreshold;
|
bool isCurrentlyVisible = (Time.time - _lastVisibleTime) < VisibilityThreshold;
|
||||||
|
|
||||||
// 3. 상태에 따라 색상과 렌더러 상태를 결정합니다.
|
|
||||||
if (_renderer.enabled == false) return;
|
|
||||||
|
|
||||||
_renderer.GetPropertyBlock(_propBlock);
|
_renderer.GetPropertyBlock(_propBlock);
|
||||||
// 2. 시야 내에 있으면 원본 색상(_originalColor), 멀어지면 어둡게 만든 색상을 적용합니다.
|
|
||||||
Color targetColor = isCurrentlyVisible ? _originalColor : _originalColor * darkIntensity;
|
Color targetColor = isCurrentlyVisible ? _originalColor : _originalColor * darkIntensity;
|
||||||
_propBlock.SetColor("_BaseColor", targetColor);
|
_propBlock.SetColor("_BaseColor", targetColor);
|
||||||
|
|
||||||
_renderer.SetPropertyBlock(_propBlock);
|
_renderer.SetPropertyBlock(_propBlock);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RevealBlock() // 서버에서 호출
|
/// <summary>
|
||||||
|
/// Reveal this block (called by server).
|
||||||
|
/// </summary>
|
||||||
|
public void RevealBlock()
|
||||||
{
|
{
|
||||||
if (IsServer && !isDiscovered.Value) isDiscovered.Value = true;
|
if (IsServer && !isDiscovered.Value)
|
||||||
|
{
|
||||||
|
isDiscovered.Value = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 플레이어가 주변을 훑을 때 호출해줄 함수
|
/// <summary>
|
||||||
|
/// Update local visibility for fog of war.
|
||||||
|
/// </summary>
|
||||||
public void UpdateLocalVisibility()
|
public void UpdateLocalVisibility()
|
||||||
{
|
{
|
||||||
_lastVisibleTime = Time.time;
|
_lastVisibleTime = Time.time;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 서버에서만 대미지를 처리하도록 제한
|
private void HandleDamaged(DamageInfo info)
|
||||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
|
||||||
public void TakeDamageRpc(int damageAmount)
|
|
||||||
{
|
{
|
||||||
if (_currentHp.Value <= 0) return;
|
// Play hit effect on all clients
|
||||||
|
PlayHitEffectClientRpc();
|
||||||
|
}
|
||||||
|
|
||||||
_currentHp.Value -= damageAmount;
|
private void HandleDeath()
|
||||||
|
{
|
||||||
if (_currentHp.Value <= 0)
|
if (IsServer)
|
||||||
{
|
{
|
||||||
DestroyBlock();
|
DropItem();
|
||||||
|
GetComponent<NetworkObject>().Despawn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DestroyBlock()
|
/// <summary>
|
||||||
{
|
/// Set highlight state for targeting feedback.
|
||||||
DropItem();
|
/// </summary>
|
||||||
// 2. 서버에서 네트워크 오브젝트 제거 (모든 클라이언트에서 사라짐)
|
|
||||||
GetComponent<NetworkObject>().Despawn();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 하이라이트 상태를 설정하는 공개 메서드
|
|
||||||
public void SetHighlight(bool isOn)
|
public void SetHighlight(bool isOn)
|
||||||
{
|
{
|
||||||
if (_outline == null) return;
|
if (_outline != null)
|
||||||
|
{
|
||||||
// 외곽선 컴포넌트 활성화/비활성화
|
_outline.enabled = isOn;
|
||||||
_outline.enabled = isOn;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 서버에서 호출하여 모든 클라이언트에게 흔들림 지시
|
/// <summary>
|
||||||
|
/// Play hit visual effect on all clients.
|
||||||
|
/// </summary>
|
||||||
[ClientRpc]
|
[ClientRpc]
|
||||||
public void PlayHitEffectClientRpc()
|
public void PlayHitEffectClientRpc()
|
||||||
{
|
{
|
||||||
@@ -181,14 +214,9 @@ public class MineableBlock : NetworkBehaviour
|
|||||||
{
|
{
|
||||||
Vector3 randomOffset = Random.insideUnitSphere * shakeMagnitude;
|
Vector3 randomOffset = Random.insideUnitSphere * shakeMagnitude;
|
||||||
transform.localPosition = _originalPos + randomOffset;
|
transform.localPosition = _originalPos + randomOffset;
|
||||||
|
|
||||||
// 좌표가 실제로 바뀌고 있는지 로그 출력
|
|
||||||
// Debug.Log($"현재 좌표: {transform.localPosition}");
|
|
||||||
|
|
||||||
elapsed += Time.deltaTime;
|
elapsed += Time.deltaTime;
|
||||||
yield return null;
|
yield return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
transform.localPosition = _originalPos;
|
transform.localPosition = _originalPos;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,13 +224,11 @@ public class MineableBlock : NetworkBehaviour
|
|||||||
{
|
{
|
||||||
if (!IsServer || dropItemData == null || genericDropPrefab == null) return;
|
if (!IsServer || dropItemData == null || genericDropPrefab == null) return;
|
||||||
|
|
||||||
// 원본 블록이 아니라 '범용 컨테이너'를 소환합니다.
|
|
||||||
GameObject dropObj = Instantiate(genericDropPrefab, transform.position + Vector3.up * 0.5f, Quaternion.identity);
|
GameObject dropObj = Instantiate(genericDropPrefab, transform.position + Vector3.up * 0.5f, Quaternion.identity);
|
||||||
|
|
||||||
NetworkObject netObj = dropObj.GetComponent<NetworkObject>();
|
NetworkObject netObj = dropObj.GetComponent<NetworkObject>();
|
||||||
netObj.Spawn();
|
netObj.Spawn();
|
||||||
|
|
||||||
// 소환된 컨테이너에 "너는 어떤 아이템의 모양을 따라해야 해"라고 알려줍니다.
|
|
||||||
if (dropObj.TryGetComponent<DroppedItem>(out var droppedItem))
|
if (dropObj.TryGetComponent<DroppedItem>(out var droppedItem))
|
||||||
{
|
{
|
||||||
droppedItem.Initialize(dropItemData.itemID);
|
droppedItem.Initialize(dropItemData.itemID);
|
||||||
@@ -212,7 +238,11 @@ public class MineableBlock : NetworkBehaviour
|
|||||||
private void UpdateVisuals(bool discovered)
|
private void UpdateVisuals(bool discovered)
|
||||||
{
|
{
|
||||||
if (_renderer != null) _renderer.enabled = discovered;
|
if (_renderer != null) _renderer.enabled = discovered;
|
||||||
// 발견되지 않은 블록은 아웃라인도 표시되지 않아야 함
|
|
||||||
if (!discovered && _outline != null) _outline.enabled = false;
|
if (!discovered && _outline != null) _outline.enabled = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the health component for direct access if needed.
|
||||||
|
/// </summary>
|
||||||
|
public HealthComponent Health => _health;
|
||||||
|
}
|
||||||
|
|||||||
20
Assets/Scripts/Player/BehaviorActionData.cs
Normal file
20
Assets/Scripts/Player/BehaviorActionData.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bridge class that wraps ItemBehavior for use with PlayerActionHandler.
|
||||||
|
/// This allows the new behavior system to work with the existing action handler.
|
||||||
|
/// </summary>
|
||||||
|
[CreateAssetMenu(menuName = "Actions/Behavior Action")]
|
||||||
|
public class BehaviorActionData : PlayerActionData
|
||||||
|
{
|
||||||
|
[HideInInspector]
|
||||||
|
public ItemBehavior behavior;
|
||||||
|
|
||||||
|
public override void ExecuteEffect(GameObject performer, GameObject target)
|
||||||
|
{
|
||||||
|
if (behavior != null)
|
||||||
|
{
|
||||||
|
behavior.Use(performer, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Scripts/Player/BehaviorActionData.cs.meta
Normal file
2
Assets/Scripts/Player/BehaviorActionData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4a373ecb07ad66848923d4a455b6d236
|
||||||
50
Assets/Scripts/Player/ConsumableBehavior.cs
Normal file
50
Assets/Scripts/Player/ConsumableBehavior.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Consumable behavior for healing items, food, etc.
|
||||||
|
/// </summary>
|
||||||
|
[CreateAssetMenu(menuName = "Items/Behaviors/Consumable Behavior")]
|
||||||
|
public class ConsumableBehavior : ItemBehavior
|
||||||
|
{
|
||||||
|
[Header("Consumable Settings")]
|
||||||
|
[SerializeField] private float healAmount = 20f;
|
||||||
|
[SerializeField] private float staminaRestore = 0f;
|
||||||
|
|
||||||
|
public override bool IsConsumable => true;
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(behaviorName)) behaviorName = "Consume";
|
||||||
|
if (string.IsNullOrEmpty(animTrigger)) animTrigger = "Consume";
|
||||||
|
canRepeat = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanUse(GameObject user, GameObject target)
|
||||||
|
{
|
||||||
|
// Can use if user has a health component that isn't at full health
|
||||||
|
var health = user.GetComponent<HealthComponent>();
|
||||||
|
if (health == null) return false;
|
||||||
|
|
||||||
|
// Can use if not at full health (healing) or if it restores stamina
|
||||||
|
return !health.IsAtFullHealth() || staminaRestore > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Use(GameObject user, GameObject target)
|
||||||
|
{
|
||||||
|
var health = user.GetComponent<HealthComponent>();
|
||||||
|
if (health != null && healAmount > 0)
|
||||||
|
{
|
||||||
|
health.Heal(healAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stamina restoration would go here when stamina system is implemented
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string GetBlockedReason(GameObject user, GameObject target)
|
||||||
|
{
|
||||||
|
var health = user.GetComponent<HealthComponent>();
|
||||||
|
if (health == null) return "Cannot use this item";
|
||||||
|
if (health.IsAtFullHealth() && staminaRestore <= 0) return "Already at full health";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Scripts/Player/ConsumableBehavior.cs.meta
Normal file
2
Assets/Scripts/Player/ConsumableBehavior.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e9c4b3ac4b03db34fa97481232baadfe
|
||||||
230
Assets/Scripts/Player/EquipmentSlot.cs
Normal file
230
Assets/Scripts/Player/EquipmentSlot.cs
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Types of equipment slots available.
|
||||||
|
/// </summary>
|
||||||
|
public enum EquipmentSlotType
|
||||||
|
{
|
||||||
|
MainHand,
|
||||||
|
OffHand,
|
||||||
|
Head,
|
||||||
|
Body,
|
||||||
|
Back,
|
||||||
|
Accessory
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a single equipment slot that can hold an equipped item.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public class EquipmentSlot
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Type of this equipment slot.
|
||||||
|
/// </summary>
|
||||||
|
public EquipmentSlotType SlotType;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transform where equipment is attached.
|
||||||
|
/// </summary>
|
||||||
|
public Transform AttachPoint;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Currently spawned equipment instance.
|
||||||
|
/// </summary>
|
||||||
|
[NonSerialized]
|
||||||
|
public GameObject CurrentEquipment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Currently equipped item data.
|
||||||
|
/// </summary>
|
||||||
|
[NonSerialized]
|
||||||
|
public ItemData EquippedItem;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event fired when equipment changes.
|
||||||
|
/// </summary>
|
||||||
|
public event Action<EquipmentSlot, ItemData> OnEquipmentChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Equip an item to this slot.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">Item to equip (or null to unequip)</param>
|
||||||
|
public void Equip(ItemData item)
|
||||||
|
{
|
||||||
|
// Remove current equipment
|
||||||
|
Unequip();
|
||||||
|
|
||||||
|
if (item == null) return;
|
||||||
|
|
||||||
|
// Check if item can be equipped
|
||||||
|
if (!item.CanBeEquipped) return;
|
||||||
|
|
||||||
|
EquippedItem = item;
|
||||||
|
|
||||||
|
// Spawn equipment visual
|
||||||
|
var prefab = item.GetEquipmentPrefab();
|
||||||
|
if (prefab != null && AttachPoint != null)
|
||||||
|
{
|
||||||
|
CurrentEquipment = UnityEngine.Object.Instantiate(prefab, AttachPoint);
|
||||||
|
CurrentEquipment.transform.localPosition = item.GetPositionOffset();
|
||||||
|
CurrentEquipment.transform.localRotation = Quaternion.Euler(item.GetRotationOffset());
|
||||||
|
}
|
||||||
|
|
||||||
|
OnEquipmentChanged?.Invoke(this, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Equip using IEquippableItem interface (more generic).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="equippable">Equippable item</param>
|
||||||
|
/// <param name="user">The user equipping the item</param>
|
||||||
|
public void Equip(IEquippableItem equippable, GameObject user)
|
||||||
|
{
|
||||||
|
Unequip();
|
||||||
|
|
||||||
|
if (equippable == null) return;
|
||||||
|
|
||||||
|
var prefab = equippable.GetEquipmentPrefab();
|
||||||
|
if (prefab == null) return;
|
||||||
|
|
||||||
|
// Find or use the configured attach point
|
||||||
|
Transform attachTo = AttachPoint;
|
||||||
|
if (attachTo == null && user != null)
|
||||||
|
{
|
||||||
|
attachTo = equippable.FindAttachmentPoint(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachTo != null)
|
||||||
|
{
|
||||||
|
CurrentEquipment = UnityEngine.Object.Instantiate(prefab, attachTo);
|
||||||
|
CurrentEquipment.transform.localPosition = equippable.GetPositionOffset();
|
||||||
|
CurrentEquipment.transform.localRotation = Quaternion.Euler(equippable.GetRotationOffset());
|
||||||
|
}
|
||||||
|
|
||||||
|
OnEquipmentChanged?.Invoke(this, EquippedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unequip the current item.
|
||||||
|
/// </summary>
|
||||||
|
public void Unequip()
|
||||||
|
{
|
||||||
|
if (CurrentEquipment != null)
|
||||||
|
{
|
||||||
|
UnityEngine.Object.Destroy(CurrentEquipment);
|
||||||
|
CurrentEquipment = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var previousItem = EquippedItem;
|
||||||
|
EquippedItem = null;
|
||||||
|
|
||||||
|
if (previousItem != null)
|
||||||
|
{
|
||||||
|
OnEquipmentChanged?.Invoke(this, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if this slot has equipment.
|
||||||
|
/// </summary>
|
||||||
|
public bool HasEquipment => CurrentEquipment != null || EquippedItem != null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if a specific item can be equipped in this slot.
|
||||||
|
/// </summary>
|
||||||
|
public bool CanEquip(ItemData item)
|
||||||
|
{
|
||||||
|
if (item == null) return true; // Can always "unequip"
|
||||||
|
return item.CanBeEquipped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages multiple equipment slots for a character.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public class EquipmentManager
|
||||||
|
{
|
||||||
|
[SerializeField]
|
||||||
|
private EquipmentSlot[] _slots;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All equipment slots.
|
||||||
|
/// </summary>
|
||||||
|
public EquipmentSlot[] Slots => _slots;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a slot by type.
|
||||||
|
/// </summary>
|
||||||
|
public EquipmentSlot GetSlot(EquipmentSlotType type)
|
||||||
|
{
|
||||||
|
if (_slots == null) return null;
|
||||||
|
|
||||||
|
foreach (var slot in _slots)
|
||||||
|
{
|
||||||
|
if (slot.SlotType == type)
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Equip an item to the appropriate slot.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryEquip(ItemData item, EquipmentSlotType preferredSlot = EquipmentSlotType.MainHand)
|
||||||
|
{
|
||||||
|
var slot = GetSlot(preferredSlot);
|
||||||
|
if (slot != null && slot.CanEquip(item))
|
||||||
|
{
|
||||||
|
slot.Equip(item);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unequip all slots.
|
||||||
|
/// </summary>
|
||||||
|
public void UnequipAll()
|
||||||
|
{
|
||||||
|
if (_slots == null) return;
|
||||||
|
|
||||||
|
foreach (var slot in _slots)
|
||||||
|
{
|
||||||
|
slot.Unequip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize slots with attach points found on the character.
|
||||||
|
/// </summary>
|
||||||
|
public void Initialize(GameObject character, params (EquipmentSlotType type, string attachPointName)[] slotConfigs)
|
||||||
|
{
|
||||||
|
_slots = new EquipmentSlot[slotConfigs.Length];
|
||||||
|
|
||||||
|
for (int i = 0; i < slotConfigs.Length; i++)
|
||||||
|
{
|
||||||
|
var config = slotConfigs[i];
|
||||||
|
Transform attachPoint = null;
|
||||||
|
|
||||||
|
// Find attach point
|
||||||
|
var transforms = character.GetComponentsInChildren<Transform>();
|
||||||
|
foreach (var t in transforms)
|
||||||
|
{
|
||||||
|
if (t.name == config.attachPointName)
|
||||||
|
{
|
||||||
|
attachPoint = t;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_slots[i] = new EquipmentSlot
|
||||||
|
{
|
||||||
|
SlotType = config.type,
|
||||||
|
AttachPoint = attachPoint
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Scripts/Player/EquipmentSlot.cs.meta
Normal file
2
Assets/Scripts/Player/EquipmentSlot.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b2bee3e86fbe00446b94cf38066b8a81
|
||||||
@@ -1,13 +1,171 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Requirement for performing an action (item cost, etc.).
|
||||||
|
/// </summary>
|
||||||
|
[System.Serializable]
|
||||||
|
public struct ActionRequirement
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Item ID required (use -1 if no item required).
|
||||||
|
/// </summary>
|
||||||
|
public int ItemID;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Amount of the item required.
|
||||||
|
/// </summary>
|
||||||
|
public int Amount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the item is consumed when the action is performed.
|
||||||
|
/// </summary>
|
||||||
|
public bool ConsumeOnUse;
|
||||||
|
|
||||||
|
public ActionRequirement(int itemID, int amount, bool consumeOnUse = true)
|
||||||
|
{
|
||||||
|
ItemID = itemID;
|
||||||
|
Amount = amount;
|
||||||
|
ConsumeOnUse = consumeOnUse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// No requirement.
|
||||||
|
/// </summary>
|
||||||
|
public static ActionRequirement None => new ActionRequirement(-1, 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Describes an action that can be performed.
|
||||||
|
/// </summary>
|
||||||
[System.Serializable]
|
[System.Serializable]
|
||||||
public class ActionDescriptor
|
public class ActionDescriptor
|
||||||
{
|
{
|
||||||
public float duration = 0.5f;
|
/// <summary>
|
||||||
public string animTrigger = "Interact";
|
/// Display name of the action.
|
||||||
// 필요하다면 여기에 사운드 이펙트나 파티클 정보를 추가할 수 있습니다.
|
/// </summary>
|
||||||
|
public string ActionName = "Action";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total duration of the action in seconds.
|
||||||
|
/// </summary>
|
||||||
|
public float Duration = 0.5f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Animation trigger name.
|
||||||
|
/// </summary>
|
||||||
|
public string AnimTrigger = "Interact";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Animation playback speed multiplier.
|
||||||
|
/// </summary>
|
||||||
|
public float AnimSpeed = 1f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Time within the animation when the effect occurs (for syncing hit with animation).
|
||||||
|
/// </summary>
|
||||||
|
public float ImpactDelay = 0f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sound effect to play.
|
||||||
|
/// </summary>
|
||||||
|
public AudioClip SoundEffect;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Particle effect prefab to spawn.
|
||||||
|
/// </summary>
|
||||||
|
public GameObject ParticleEffect;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stamina cost to perform this action.
|
||||||
|
/// </summary>
|
||||||
|
public float StaminaCost = 0f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Item requirements for this action.
|
||||||
|
/// </summary>
|
||||||
|
public ActionRequirement[] ItemRequirements;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this action can be repeated by holding the button.
|
||||||
|
/// </summary>
|
||||||
|
public bool CanRepeat = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cooldown time before this action can be performed again.
|
||||||
|
/// </summary>
|
||||||
|
public float Cooldown = 0f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a simple action descriptor.
|
||||||
|
/// </summary>
|
||||||
|
public static ActionDescriptor Simple(string name, float duration, string animTrigger = "Interact")
|
||||||
|
{
|
||||||
|
return new ActionDescriptor
|
||||||
|
{
|
||||||
|
ActionName = name,
|
||||||
|
Duration = duration,
|
||||||
|
AnimTrigger = animTrigger
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create an action descriptor for repeatable actions (like mining).
|
||||||
|
/// </summary>
|
||||||
|
public static ActionDescriptor Repeatable(string name, float duration, string animTrigger,
|
||||||
|
float impactDelay, float animSpeed = 1f)
|
||||||
|
{
|
||||||
|
return new ActionDescriptor
|
||||||
|
{
|
||||||
|
ActionName = name,
|
||||||
|
Duration = duration,
|
||||||
|
AnimTrigger = animTrigger,
|
||||||
|
AnimSpeed = animSpeed,
|
||||||
|
ImpactDelay = impactDelay,
|
||||||
|
CanRepeat = true
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 명세를 제공하는 인터페이스
|
/// <summary>
|
||||||
|
/// Interface for objects that can provide action descriptors.
|
||||||
|
/// Implement this to define what actions can be performed on or with an object.
|
||||||
|
/// </summary>
|
||||||
public interface IActionProvider
|
public interface IActionProvider
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get the primary action descriptor for this provider.
|
||||||
|
/// </summary>
|
||||||
ActionDescriptor GetActionDescriptor();
|
ActionDescriptor GetActionDescriptor();
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all available actions from this provider.
|
||||||
|
/// Default implementation returns only the primary action.
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<ActionDescriptor> GetAvailableActions()
|
||||||
|
{
|
||||||
|
yield return GetActionDescriptor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if a specific action can be performed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="performer">The GameObject attempting the action</param>
|
||||||
|
/// <param name="action">The action to check</param>
|
||||||
|
/// <returns>True if the action can be performed</returns>
|
||||||
|
bool CanPerformAction(GameObject performer, ActionDescriptor action)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the reason why an action cannot be performed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="performer">The GameObject attempting the action</param>
|
||||||
|
/// <param name="action">The action to check</param>
|
||||||
|
/// <returns>Reason string, or null if action can be performed</returns>
|
||||||
|
string GetActionBlockedReason(GameObject performer, ActionDescriptor action)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ public class MiningActionData : PlayerActionData
|
|||||||
|
|
||||||
public override void ExecuteEffect(GameObject performer, GameObject target)
|
public override void ExecuteEffect(GameObject performer, GameObject target)
|
||||||
{
|
{
|
||||||
if(target == null) return;
|
if (target == null) return;
|
||||||
|
|
||||||
if (target.TryGetComponent<MineableBlock>(out var block))
|
// Use IDamageable interface for all damageable objects
|
||||||
|
if (target.TryGetComponent<IDamageable>(out var damageable))
|
||||||
{
|
{
|
||||||
// 서버 RPC 호출은 블록 내부의 로직을 그대로 사용합니다.
|
damageable.TakeDamage(new DamageInfo(damage, DamageType.Mining, performer));
|
||||||
block.TakeDamageRpc(damage);
|
|
||||||
block.PlayHitEffectClientRpc();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
Assets/Scripts/Player/MiningBehavior.cs
Normal file
44
Assets/Scripts/Player/MiningBehavior.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mining behavior for pickaxes and similar tools.
|
||||||
|
/// </summary>
|
||||||
|
[CreateAssetMenu(menuName = "Items/Behaviors/Mining Behavior")]
|
||||||
|
public class MiningBehavior : ItemBehavior
|
||||||
|
{
|
||||||
|
[Header("Mining Settings")]
|
||||||
|
[SerializeField] private int damage = 50;
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
// Set default mining values
|
||||||
|
if (string.IsNullOrEmpty(behaviorName)) behaviorName = "Mine";
|
||||||
|
if (string.IsNullOrEmpty(animTrigger)) animTrigger = "Attack";
|
||||||
|
canRepeat = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanUse(GameObject user, GameObject target)
|
||||||
|
{
|
||||||
|
// Can always swing, but only deals damage if hitting a mineable block
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Use(GameObject user, GameObject target)
|
||||||
|
{
|
||||||
|
if (target == null) return;
|
||||||
|
|
||||||
|
// Use IDamageable interface for all damageable objects
|
||||||
|
if (target.TryGetComponent<IDamageable>(out var damageable))
|
||||||
|
{
|
||||||
|
damageable.TakeDamage(new DamageInfo(damage, DamageType.Mining, user));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string GetBlockedReason(GameObject user, GameObject target)
|
||||||
|
{
|
||||||
|
if (target == null) return "No target";
|
||||||
|
if (!target.TryGetComponent<IDamageable>(out _))
|
||||||
|
return "Cannot mine this object";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Scripts/Player/MiningBehavior.cs.meta
Normal file
2
Assets/Scripts/Player/MiningBehavior.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c1deeb9de56edff4ca77ddabf9db691a
|
||||||
41
Assets/Scripts/Player/PlaceableBehavior.cs
Normal file
41
Assets/Scripts/Player/PlaceableBehavior.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Placeable behavior for building/placing items.
|
||||||
|
/// </summary>
|
||||||
|
[CreateAssetMenu(menuName = "Items/Behaviors/Placeable Behavior")]
|
||||||
|
public class PlaceableBehavior : ItemBehavior
|
||||||
|
{
|
||||||
|
[Header("Placement Settings")]
|
||||||
|
[SerializeField] private GameObject placeablePrefab;
|
||||||
|
[SerializeField] private bool requiresGround = true;
|
||||||
|
[SerializeField] private float placementRange = 5f;
|
||||||
|
|
||||||
|
public override bool IsConsumable => true;
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(behaviorName)) behaviorName = "Place";
|
||||||
|
if (string.IsNullOrEmpty(animTrigger)) animTrigger = "Place";
|
||||||
|
canRepeat = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanUse(GameObject user, GameObject target)
|
||||||
|
{
|
||||||
|
// Would integrate with BuildManager for placement validation
|
||||||
|
return placeablePrefab != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Use(GameObject user, GameObject target)
|
||||||
|
{
|
||||||
|
// Actual placement would be handled by BuildManager
|
||||||
|
// This is a placeholder for the behavior pattern
|
||||||
|
Debug.Log($"[PlaceableBehavior] Would place {placeablePrefab?.name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string GetBlockedReason(GameObject user, GameObject target)
|
||||||
|
{
|
||||||
|
if (placeablePrefab == null) return "Invalid placement item";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Scripts/Player/PlaceableBehavior.cs.meta
Normal file
2
Assets/Scripts/Player/PlaceableBehavior.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b4962abe690c6ef47b7ea654ce747200
|
||||||
@@ -33,11 +33,11 @@ public class PlayerActionHandler : NetworkBehaviour
|
|||||||
private IEnumerator InteractionRoutine(ActionDescriptor desc, IInteractable target)
|
private IEnumerator InteractionRoutine(ActionDescriptor desc, IInteractable target)
|
||||||
{
|
{
|
||||||
_isBusy = true;
|
_isBusy = true;
|
||||||
if (desc != null) _animator.SetTrigger(desc.animTrigger);
|
if (desc != null) _animator.SetTrigger(desc.AnimTrigger);
|
||||||
|
|
||||||
target.Interact(gameObject); // 로직 실행
|
target.Interact(gameObject); // 로직 실행
|
||||||
|
|
||||||
yield return new WaitForSeconds(desc?.duration ?? 0.1f);
|
yield return new WaitForSeconds(desc?.Duration ?? 0.1f);
|
||||||
_isBusy = false;
|
_isBusy = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,63 @@
|
|||||||
using Unity.Netcode;
|
using Unity.Netcode;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles equipment visuals for the player.
|
||||||
|
/// Uses the new EquipmentSlot system while maintaining backwards compatibility.
|
||||||
|
/// </summary>
|
||||||
public class PlayerEquipmentHandler : NetworkBehaviour
|
public class PlayerEquipmentHandler : NetworkBehaviour
|
||||||
{
|
{
|
||||||
[SerializeField] private Transform toolAnchor; // 캐릭터 손의 소켓 위치
|
[Header("Equipment Settings")]
|
||||||
|
[SerializeField] private Transform mainHandAnchor;
|
||||||
|
[SerializeField] private Transform offHandAnchor;
|
||||||
|
|
||||||
|
[Header("Slot Configuration")]
|
||||||
|
[SerializeField] private EquipmentSlot mainHandSlot;
|
||||||
|
|
||||||
private PlayerInventory _inventory;
|
private PlayerInventory _inventory;
|
||||||
private GameObject _currentToolInstance; // 현재 생성된 도구 모델
|
private GameObject _currentToolInstance;
|
||||||
|
|
||||||
void Awake()
|
void Awake()
|
||||||
{
|
{
|
||||||
_inventory = GetComponent<PlayerInventory>();
|
_inventory = GetComponent<PlayerInventory>();
|
||||||
|
|
||||||
|
// Initialize main hand slot if not configured in inspector
|
||||||
|
if (mainHandSlot == null)
|
||||||
|
{
|
||||||
|
mainHandSlot = new EquipmentSlot
|
||||||
|
{
|
||||||
|
SlotType = EquipmentSlotType.MainHand,
|
||||||
|
AttachPoint = mainHandAnchor
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (mainHandSlot.AttachPoint == null)
|
||||||
|
{
|
||||||
|
mainHandSlot.AttachPoint = mainHandAnchor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnNetworkSpawn()
|
public override void OnNetworkSpawn()
|
||||||
{
|
{
|
||||||
// 인벤토리의 슬롯 변경 이벤트 구독
|
// Subscribe to inventory slot changes
|
||||||
// OnSlotChanged는 (이전 값, 새 값) 두 개의 인자를 전달합니다.
|
if (_inventory != null)
|
||||||
_inventory.OnSlotChanged += HandleSlotChanged;
|
{
|
||||||
|
_inventory.OnSlotChanged += HandleSlotChanged;
|
||||||
|
|
||||||
// 게임 시작 시 처음에 들고 있는 아이템 모델 생성
|
// Initialize with current slot
|
||||||
UpdateEquippedModel(_inventory.SelectedSlotIndex);
|
UpdateEquippedModel(_inventory.SelectedSlotIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnNetworkDespawn()
|
||||||
|
{
|
||||||
|
// Unsubscribe to prevent memory leaks
|
||||||
|
if (_inventory != null)
|
||||||
|
{
|
||||||
|
_inventory.OnSlotChanged -= HandleSlotChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up equipment
|
||||||
|
mainHandSlot?.Unequip();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleSlotChanged(int previousValue, int newValue)
|
private void HandleSlotChanged(int previousValue, int newValue)
|
||||||
@@ -29,30 +67,58 @@ public class PlayerEquipmentHandler : NetworkBehaviour
|
|||||||
|
|
||||||
private void UpdateEquippedModel(int slotIndex)
|
private void UpdateEquippedModel(int slotIndex)
|
||||||
{
|
{
|
||||||
// 1. 기존 도구가 있다면 파괴
|
// Get item data for the selected slot
|
||||||
if (_currentToolInstance != null)
|
ItemData data = _inventory?.GetItemDataInSlot(slotIndex);
|
||||||
|
|
||||||
|
// Use new equipment slot system
|
||||||
|
if (data != null && data.CanBeEquipped)
|
||||||
{
|
{
|
||||||
Destroy(_currentToolInstance);
|
// Use IEquippableItem interface
|
||||||
|
mainHandSlot.Equip(data);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mainHandSlot.Unequip();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 현재 선택된 슬롯의 데이터 확인
|
// Update legacy reference for any code that might check it
|
||||||
ItemData data = _inventory.GetItemDataInSlot(slotIndex);
|
_currentToolInstance = mainHandSlot.CurrentEquipment;
|
||||||
|
|
||||||
// 3. 도구인 경우에만 모델 생성
|
|
||||||
if (data != null && data.isTool && data.toolPrefab != null)
|
|
||||||
{
|
|
||||||
_currentToolInstance = Instantiate(data.toolPrefab, toolAnchor);
|
|
||||||
|
|
||||||
// ItemData에 설정된 오프셋 적용
|
|
||||||
_currentToolInstance.transform.localPosition = data.equipPositionOffset;
|
|
||||||
_currentToolInstance.transform.localRotation = Quaternion.Euler(data.equipRotationOffset);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnNetworkDespawn()
|
/// <summary>
|
||||||
|
/// Get the currently equipped item data.
|
||||||
|
/// </summary>
|
||||||
|
public ItemData GetEquippedItem()
|
||||||
{
|
{
|
||||||
// 이벤트 구독 해제 (메모리 누수 방지)
|
return mainHandSlot?.EquippedItem;
|
||||||
if (_inventory != null)
|
|
||||||
_inventory.OnSlotChanged -= HandleSlotChanged;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the currently equipped tool instance.
|
||||||
|
/// </summary>
|
||||||
|
public GameObject GetCurrentToolInstance()
|
||||||
|
{
|
||||||
|
return mainHandSlot?.CurrentEquipment ?? _currentToolInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if player has equipment in main hand.
|
||||||
|
/// </summary>
|
||||||
|
public bool HasMainHandEquipment => mainHandSlot?.HasEquipment ?? false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Force refresh the equipped model.
|
||||||
|
/// </summary>
|
||||||
|
public void RefreshEquipment()
|
||||||
|
{
|
||||||
|
if (_inventory != null)
|
||||||
|
{
|
||||||
|
UpdateEquippedModel(_inventory.SelectedSlotIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the main hand equipment slot for advanced usage.
|
||||||
|
/// </summary>
|
||||||
|
public EquipmentSlot MainHandSlot => mainHandSlot;
|
||||||
|
}
|
||||||
|
|||||||
@@ -204,35 +204,50 @@ public class PlayerNetworkController : NetworkBehaviour
|
|||||||
if (_isGrounded) _velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
|
if (_isGrounded) _velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 액션 (좌클릭) - 대상이 없어도 나감
|
// 1. Action (Left Click) - executes even without target
|
||||||
// PlayerNetworkController.cs 중 일부
|
|
||||||
private void OnActionInput()
|
private void OnActionInput()
|
||||||
{
|
{
|
||||||
if (!IsOwner || _actionHandler.IsBusy) return;
|
if (!IsOwner || _actionHandler.IsBusy) return;
|
||||||
|
|
||||||
ItemData selectedItem = _inventory.GetSelectedItemData();
|
ItemData selectedItem = _inventory.GetSelectedItemData();
|
||||||
|
if (selectedItem == null) return;
|
||||||
|
|
||||||
// 로그 1: 아이템 확인
|
// Check if item has behavior (new system)
|
||||||
if (selectedItem == null) { Debug.Log("선택된 아이템이 없음"); return; }
|
if (selectedItem.behavior != null)
|
||||||
|
|
||||||
// 로그 2: 도구 여부 및 액션 데이터 확인
|
|
||||||
Debug.Log($"현재 아이템: {selectedItem.itemName}, 도구여부: {selectedItem.isTool}, 액션데이터: {selectedItem.toolAction != null}");
|
|
||||||
|
|
||||||
if (selectedItem.isTool && selectedItem.toolAction != null)
|
|
||||||
{
|
{
|
||||||
if (_lastHighlightedBlock != null)
|
GameObject target = _lastHighlightedBlock != null ? _lastHighlightedBlock.gameObject : null;
|
||||||
|
|
||||||
|
// Use the new behavior system
|
||||||
|
if (selectedItem.CanUse(gameObject, target))
|
||||||
{
|
{
|
||||||
Debug.Log($"채광 시작: {_lastHighlightedBlock.name}");
|
// Get action descriptor and perform action
|
||||||
_actionHandler.PerformAction(selectedItem.toolAction, _lastHighlightedBlock.gameObject);
|
var actionDesc = selectedItem.GetUseAction();
|
||||||
}
|
if (actionDesc != null)
|
||||||
else
|
{
|
||||||
{
|
_actionHandler.PerformAction(
|
||||||
Debug.Log("조준된 블록이 없음 (하이라이트 확인 필요)");
|
CreateActionDataFromDescriptor(actionDesc, selectedItem.behavior),
|
||||||
_actionHandler.PerformAction(selectedItem.toolAction, null);
|
target
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to bridge between new ActionDescriptor and legacy PlayerActionData
|
||||||
|
private PlayerActionData CreateActionDataFromDescriptor(ActionDescriptor desc, ItemBehavior behavior)
|
||||||
|
{
|
||||||
|
// Create a temporary runtime action data
|
||||||
|
var actionData = ScriptableObject.CreateInstance<BehaviorActionData>();
|
||||||
|
actionData.actionName = desc.ActionName;
|
||||||
|
actionData.duration = desc.Duration;
|
||||||
|
actionData.animTrigger = desc.AnimTrigger;
|
||||||
|
actionData.impactDelay = desc.ImpactDelay;
|
||||||
|
actionData.baseSpeed = desc.AnimSpeed;
|
||||||
|
actionData.canRepeat = desc.CanRepeat;
|
||||||
|
actionData.behavior = behavior;
|
||||||
|
return actionData;
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 인터랙션 (F키) - 대상이 없으면 아예 시작 안 함
|
// 2. 인터랙션 (F키) - 대상이 없으면 아예 시작 안 함
|
||||||
private void OnInteractTap()
|
private void OnInteractTap()
|
||||||
{
|
{
|
||||||
@@ -250,13 +265,13 @@ public class PlayerNetworkController : NetworkBehaviour
|
|||||||
{
|
{
|
||||||
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(targetId, out var target))
|
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(targetId, out var target))
|
||||||
{
|
{
|
||||||
if (target.TryGetComponent<MineableBlock>(out var block))
|
// Use IDamageable interface instead of MineableBlock directly
|
||||||
|
if (target.TryGetComponent<IDamageable>(out var damageable))
|
||||||
{
|
{
|
||||||
// 서버에서 최종 거리 검증 후 대미지 적용
|
// Server-side distance validation before applying damage
|
||||||
if (Vector3.Distance(transform.position, target.transform.position) <= attackRange + 1.0f)
|
if (Vector3.Distance(transform.position, target.transform.position) <= attackRange + 1.0f)
|
||||||
{
|
{
|
||||||
block.TakeDamageRpc(miningDamage);
|
damageable.TakeDamage(new DamageInfo(miningDamage, DamageType.Mining, gameObject));
|
||||||
block.PlayHitEffectClientRpc();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -487,10 +502,11 @@ public class PlayerNetworkController : NetworkBehaviour
|
|||||||
private void HandleContinuousAction()
|
private void HandleContinuousAction()
|
||||||
{
|
{
|
||||||
ItemData selectedItem = _inventory.GetSelectedItemData();
|
ItemData selectedItem = _inventory.GetSelectedItemData();
|
||||||
if (selectedItem == null || !selectedItem.isTool || selectedItem.toolAction == null) return;
|
if (selectedItem == null || selectedItem.behavior == null) return;
|
||||||
|
|
||||||
// [핵심] 반복 가능한 액션일 때만 Update에서 재실행
|
// Only repeat if action supports it
|
||||||
if (selectedItem.toolAction.canRepeat)
|
var actionDesc = selectedItem.GetUseAction();
|
||||||
|
if (actionDesc != null && actionDesc.CanRepeat)
|
||||||
{
|
{
|
||||||
TryExecuteAction();
|
TryExecuteAction();
|
||||||
}
|
}
|
||||||
@@ -501,15 +517,23 @@ public class PlayerNetworkController : NetworkBehaviour
|
|||||||
if (_actionHandler.IsBusy) return;
|
if (_actionHandler.IsBusy) return;
|
||||||
|
|
||||||
ItemData selectedItem = _inventory.GetSelectedItemData();
|
ItemData selectedItem = _inventory.GetSelectedItemData();
|
||||||
if (selectedItem != null && selectedItem.isTool && selectedItem.toolAction != null)
|
if (selectedItem == null || selectedItem.behavior == null) return;
|
||||||
|
|
||||||
|
var actionDesc = selectedItem.GetUseAction();
|
||||||
|
if (actionDesc == null) return;
|
||||||
|
|
||||||
|
// Skip if non-repeatable action already executed once
|
||||||
|
if (!actionDesc.CanRepeat && _hasExecutedOnce) return;
|
||||||
|
|
||||||
|
GameObject target = _lastHighlightedBlock != null ? _lastHighlightedBlock.gameObject : null;
|
||||||
|
|
||||||
|
if (selectedItem.CanUse(gameObject, target))
|
||||||
{
|
{
|
||||||
// 단발성 액션인데 이미 한 번 실행했다면 스킵
|
_actionHandler.PerformAction(
|
||||||
if (!selectedItem.toolAction.canRepeat && _hasExecutedOnce) return;
|
CreateActionDataFromDescriptor(actionDesc, selectedItem.behavior),
|
||||||
|
target
|
||||||
GameObject target = _lastHighlightedBlock != null ? _lastHighlightedBlock.gameObject : null;
|
);
|
||||||
_actionHandler.PerformAction(selectedItem.toolAction, target);
|
_hasExecutedOnce = true;
|
||||||
|
|
||||||
_hasExecutedOnce = true; // 실행 기록 저장
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
Assets/Scripts/Utilities.meta
Normal file
8
Assets/Scripts/Utilities.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d5b5e4be41b488d4cb306af7c3175e94
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
215
Assets/Scripts/Utilities/ActionExecutor.cs
Normal file
215
Assets/Scripts/Utilities/ActionExecutor.cs
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reusable action execution system with busy state management.
|
||||||
|
/// Replaces repeated coroutine busy-state patterns across the codebase.
|
||||||
|
/// </summary>
|
||||||
|
public class ActionExecutor : MonoBehaviour
|
||||||
|
{
|
||||||
|
public bool IsBusy { get; private set; }
|
||||||
|
|
||||||
|
private Coroutine _currentAction;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event fired when an action starts.
|
||||||
|
/// </summary>
|
||||||
|
public event Action OnActionStarted;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event fired when an action completes.
|
||||||
|
/// </summary>
|
||||||
|
public event Action OnActionCompleted;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event fired when an action is cancelled.
|
||||||
|
/// </summary>
|
||||||
|
public event Action OnActionCancelled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Try to execute an action. Returns false if already busy.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The action request to execute</param>
|
||||||
|
/// <returns>True if action started, false if busy</returns>
|
||||||
|
public bool TryExecute(ActionRequest request)
|
||||||
|
{
|
||||||
|
if (IsBusy) return false;
|
||||||
|
|
||||||
|
_currentAction = StartCoroutine(ExecuteRoutine(request));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cancel the current action if one is running.
|
||||||
|
/// </summary>
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
if (_currentAction != null)
|
||||||
|
{
|
||||||
|
StopCoroutine(_currentAction);
|
||||||
|
_currentAction = null;
|
||||||
|
IsBusy = false;
|
||||||
|
OnActionCancelled?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Force reset the busy state. Use with caution.
|
||||||
|
/// </summary>
|
||||||
|
public void ForceReset()
|
||||||
|
{
|
||||||
|
if (_currentAction != null)
|
||||||
|
{
|
||||||
|
StopCoroutine(_currentAction);
|
||||||
|
_currentAction = null;
|
||||||
|
}
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator ExecuteRoutine(ActionRequest request)
|
||||||
|
{
|
||||||
|
IsBusy = true;
|
||||||
|
OnActionStarted?.Invoke();
|
||||||
|
|
||||||
|
// Pre-action callback
|
||||||
|
request.OnStart?.Invoke();
|
||||||
|
|
||||||
|
// Animation trigger
|
||||||
|
if (request.Animator != null && !string.IsNullOrEmpty(request.AnimTrigger))
|
||||||
|
{
|
||||||
|
if (request.AnimSpeed > 0)
|
||||||
|
{
|
||||||
|
request.Animator.SetFloat("ActionSpeed", request.AnimSpeed);
|
||||||
|
}
|
||||||
|
request.Animator.SetTrigger(request.AnimTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for impact point
|
||||||
|
if (request.ImpactDelay > 0)
|
||||||
|
{
|
||||||
|
float adjustedDelay = request.AnimSpeed > 0
|
||||||
|
? request.ImpactDelay / request.AnimSpeed
|
||||||
|
: request.ImpactDelay;
|
||||||
|
yield return new WaitForSeconds(adjustedDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute main effect
|
||||||
|
request.OnImpact?.Invoke();
|
||||||
|
|
||||||
|
// Wait for remaining duration
|
||||||
|
float remainingTime = request.TotalDuration - request.ImpactDelay;
|
||||||
|
if (request.AnimSpeed > 0)
|
||||||
|
{
|
||||||
|
remainingTime /= request.AnimSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingTime > 0)
|
||||||
|
{
|
||||||
|
yield return new WaitForSeconds(remainingTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Completion callback
|
||||||
|
request.OnComplete?.Invoke();
|
||||||
|
|
||||||
|
IsBusy = false;
|
||||||
|
_currentAction = null;
|
||||||
|
OnActionCompleted?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration for an action to be executed by ActionExecutor.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public struct ActionRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Animator to trigger animations on.
|
||||||
|
/// </summary>
|
||||||
|
public Animator Animator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Animation trigger name.
|
||||||
|
/// </summary>
|
||||||
|
public string AnimTrigger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Animation playback speed multiplier.
|
||||||
|
/// </summary>
|
||||||
|
public float AnimSpeed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Time delay before the impact/effect happens (for syncing with animation).
|
||||||
|
/// </summary>
|
||||||
|
public float ImpactDelay;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total duration of the action.
|
||||||
|
/// </summary>
|
||||||
|
public float TotalDuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Callback invoked when action starts.
|
||||||
|
/// </summary>
|
||||||
|
public Action OnStart;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Callback invoked at the impact moment.
|
||||||
|
/// </summary>
|
||||||
|
public Action OnImpact;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Callback invoked when action completes.
|
||||||
|
/// </summary>
|
||||||
|
public Action OnComplete;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a simple action request with just timing.
|
||||||
|
/// </summary>
|
||||||
|
public static ActionRequest Simple(float duration, Action onComplete)
|
||||||
|
{
|
||||||
|
return new ActionRequest
|
||||||
|
{
|
||||||
|
TotalDuration = duration,
|
||||||
|
AnimSpeed = 1f,
|
||||||
|
OnComplete = onComplete
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create an animated action request.
|
||||||
|
/// </summary>
|
||||||
|
public static ActionRequest Animated(Animator animator, string trigger, float duration,
|
||||||
|
float impactDelay, Action onImpact, float speed = 1f)
|
||||||
|
{
|
||||||
|
return new ActionRequest
|
||||||
|
{
|
||||||
|
Animator = animator,
|
||||||
|
AnimTrigger = trigger,
|
||||||
|
AnimSpeed = speed,
|
||||||
|
ImpactDelay = impactDelay,
|
||||||
|
TotalDuration = duration,
|
||||||
|
OnImpact = onImpact
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a full action request with all callbacks.
|
||||||
|
/// </summary>
|
||||||
|
public static ActionRequest Full(Animator animator, string trigger, float duration,
|
||||||
|
float impactDelay, float speed, Action onStart, Action onImpact, Action onComplete)
|
||||||
|
{
|
||||||
|
return new ActionRequest
|
||||||
|
{
|
||||||
|
Animator = animator,
|
||||||
|
AnimTrigger = trigger,
|
||||||
|
AnimSpeed = speed,
|
||||||
|
ImpactDelay = impactDelay,
|
||||||
|
TotalDuration = duration,
|
||||||
|
OnStart = onStart,
|
||||||
|
OnImpact = onImpact,
|
||||||
|
OnComplete = onComplete
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Scripts/Utilities/ActionExecutor.cs.meta
Normal file
2
Assets/Scripts/Utilities/ActionExecutor.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 27e07922480ecf44faf5135ad4872531
|
||||||
228
Assets/Scripts/Utilities/PhysicsQueryUtility.cs
Normal file
228
Assets/Scripts/Utilities/PhysicsQueryUtility.cs
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Utility class for common physics queries.
|
||||||
|
/// Eliminates repeated OverlapSphere patterns across the codebase.
|
||||||
|
/// </summary>
|
||||||
|
public static class PhysicsQueryUtility
|
||||||
|
{
|
||||||
|
// Reusable buffer to avoid allocations (32 should be enough for most cases)
|
||||||
|
private static readonly Collider[] OverlapBuffer = new Collider[32];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Find the closest object implementing interface T within radius.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Interface or component type to search for</typeparam>
|
||||||
|
/// <param name="origin">Center point of the search</param>
|
||||||
|
/// <param name="radius">Search radius</param>
|
||||||
|
/// <param name="layerMask">Layer mask to filter objects</param>
|
||||||
|
/// <returns>Closest object of type T, or null if none found</returns>
|
||||||
|
public static T FindClosest<T>(Vector3 origin, float radius, LayerMask layerMask) where T : class
|
||||||
|
{
|
||||||
|
int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask);
|
||||||
|
|
||||||
|
T closest = null;
|
||||||
|
float minDistSqr = float.MaxValue;
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
T component = OverlapBuffer[i].GetComponentInParent<T>();
|
||||||
|
if (component != null)
|
||||||
|
{
|
||||||
|
float distSqr = (origin - OverlapBuffer[i].transform.position).sqrMagnitude;
|
||||||
|
if (distSqr < minDistSqr)
|
||||||
|
{
|
||||||
|
minDistSqr = distSqr;
|
||||||
|
closest = component;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return closest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Find the closest object implementing interface T within radius, also returning distance.
|
||||||
|
/// </summary>
|
||||||
|
public static T FindClosest<T>(Vector3 origin, float radius, LayerMask layerMask, out float distance) where T : class
|
||||||
|
{
|
||||||
|
int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask);
|
||||||
|
|
||||||
|
T closest = null;
|
||||||
|
float minDistSqr = float.MaxValue;
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
T component = OverlapBuffer[i].GetComponentInParent<T>();
|
||||||
|
if (component != null)
|
||||||
|
{
|
||||||
|
float distSqr = (origin - OverlapBuffer[i].transform.position).sqrMagnitude;
|
||||||
|
if (distSqr < minDistSqr)
|
||||||
|
{
|
||||||
|
minDistSqr = distSqr;
|
||||||
|
closest = component;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
distance = closest != null ? Mathf.Sqrt(minDistSqr) : 0f;
|
||||||
|
return closest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute action on all objects of type T within radius.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Interface or component type to search for</typeparam>
|
||||||
|
/// <param name="origin">Center point of the search</param>
|
||||||
|
/// <param name="radius">Search radius</param>
|
||||||
|
/// <param name="layerMask">Layer mask to filter objects</param>
|
||||||
|
/// <param name="action">Action to execute on each found object</param>
|
||||||
|
public static void ForEachInRadius<T>(Vector3 origin, float radius, LayerMask layerMask, Action<T> action) where T : class
|
||||||
|
{
|
||||||
|
int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask);
|
||||||
|
|
||||||
|
// Track processed objects to avoid duplicates (same object hit by multiple colliders)
|
||||||
|
var processed = new HashSet<T>();
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
T component = OverlapBuffer[i].GetComponentInParent<T>();
|
||||||
|
if (component != null && !processed.Contains(component))
|
||||||
|
{
|
||||||
|
processed.Add(component);
|
||||||
|
action(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute action on all objects of type T within radius, with additional context.
|
||||||
|
/// </summary>
|
||||||
|
public static void ForEachInRadius<T>(Vector3 origin, float radius, LayerMask layerMask,
|
||||||
|
Action<T, Collider> action) where T : class
|
||||||
|
{
|
||||||
|
int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask);
|
||||||
|
|
||||||
|
var processed = new HashSet<T>();
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
T component = OverlapBuffer[i].GetComponentInParent<T>();
|
||||||
|
if (component != null && !processed.Contains(component))
|
||||||
|
{
|
||||||
|
processed.Add(component);
|
||||||
|
action(component, OverlapBuffer[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all objects of type T within radius.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Interface or component type to search for</typeparam>
|
||||||
|
/// <param name="origin">Center point of the search</param>
|
||||||
|
/// <param name="radius">Search radius</param>
|
||||||
|
/// <param name="layerMask">Layer mask to filter objects</param>
|
||||||
|
/// <returns>List of all objects of type T within radius</returns>
|
||||||
|
public static List<T> FindAllInRadius<T>(Vector3 origin, float radius, LayerMask layerMask) where T : class
|
||||||
|
{
|
||||||
|
var results = new List<T>();
|
||||||
|
int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask);
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
T component = OverlapBuffer[i].GetComponentInParent<T>();
|
||||||
|
if (component != null && !results.Contains(component))
|
||||||
|
{
|
||||||
|
results.Add(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Find all objects of type T within radius, sorted by distance (closest first).
|
||||||
|
/// </summary>
|
||||||
|
public static List<T> FindAllInRadiusSorted<T>(Vector3 origin, float radius, LayerMask layerMask) where T : class
|
||||||
|
{
|
||||||
|
int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask);
|
||||||
|
|
||||||
|
var resultsWithDistance = new List<(T component, float distSqr)>();
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
T component = OverlapBuffer[i].GetComponentInParent<T>();
|
||||||
|
if (component != null)
|
||||||
|
{
|
||||||
|
bool alreadyAdded = false;
|
||||||
|
foreach (var item in resultsWithDistance)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(item.component, component))
|
||||||
|
{
|
||||||
|
alreadyAdded = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!alreadyAdded)
|
||||||
|
{
|
||||||
|
float distSqr = (origin - OverlapBuffer[i].transform.position).sqrMagnitude;
|
||||||
|
resultsWithDistance.Add((component, distSqr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by distance
|
||||||
|
resultsWithDistance.Sort((a, b) => a.distSqr.CompareTo(b.distSqr));
|
||||||
|
|
||||||
|
var results = new List<T>(resultsWithDistance.Count);
|
||||||
|
foreach (var item in resultsWithDistance)
|
||||||
|
{
|
||||||
|
results.Add(item.component);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if any object of type T exists within radius.
|
||||||
|
/// </summary>
|
||||||
|
public static bool AnyInRadius<T>(Vector3 origin, float radius, LayerMask layerMask) where T : class
|
||||||
|
{
|
||||||
|
int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask);
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
if (OverlapBuffer[i].GetComponentInParent<T>() != null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Count objects of type T within radius.
|
||||||
|
/// </summary>
|
||||||
|
public static int CountInRadius<T>(Vector3 origin, float radius, LayerMask layerMask) where T : class
|
||||||
|
{
|
||||||
|
int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask);
|
||||||
|
|
||||||
|
var processed = new HashSet<T>();
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
T component = OverlapBuffer[i].GetComponentInParent<T>();
|
||||||
|
if (component != null)
|
||||||
|
{
|
||||||
|
processed.Add(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processed.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Scripts/Utilities/PhysicsQueryUtility.cs.meta
Normal file
2
Assets/Scripts/Utilities/PhysicsQueryUtility.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e1262d37bb47b7d4ea78d6426675ae9d
|
||||||
Reference in New Issue
Block a user