플레이어/적/몬스터 팀 시스템 생성
몬스터 및 적 AI 구현
This commit is contained in:
27
Assets/Data/BuildingData_Core.asset
Normal file
27
Assets/Data/BuildingData_Core.asset
Normal file
@@ -0,0 +1,27 @@
|
||||
%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: 937e64980d44d6b46acb35b8046adf34, type: 3}
|
||||
m_Name: BuildingData_Core
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Northbound.BuildingData
|
||||
buildingName: BaseTower
|
||||
prefab: {fileID: 3733880183385667081, guid: 1979909431408184b9bc587877c5b4b4, type: 3}
|
||||
width: 5
|
||||
length: 5
|
||||
height: 5
|
||||
placementOffset: {x: 0, y: 0, z: 0}
|
||||
allowRotation: 1
|
||||
maxHealth: 1000
|
||||
isIndestructible: 0
|
||||
autoRegenerate: 0
|
||||
regenPerSecond: 1
|
||||
providesVision: 1
|
||||
visionRange: 40
|
||||
8
Assets/Data/BuildingData_Core.asset.meta
Normal file
8
Assets/Data/BuildingData_Core.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e495d169ee3bce449f4b1aea83d6818
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -11,7 +11,7 @@ GameObject:
|
||||
- component: {fileID: 8064559726283331702}
|
||||
- component: {fileID: 5173262576415873253}
|
||||
- component: {fileID: 1287070985890992582}
|
||||
- component: {fileID: 945062474581833766}
|
||||
- component: {fileID: 2964705630284685173}
|
||||
m_Layer: 0
|
||||
m_Name: Core
|
||||
m_TagString: Untagged
|
||||
@@ -47,7 +47,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
||||
GlobalObjectIdHash: 1288120633
|
||||
GlobalObjectIdHash: 615747208
|
||||
InScenePlacedSourceGlobalObjectIdHash: 0
|
||||
DeferredDespawnTick: 0
|
||||
Ownership: 1
|
||||
@@ -85,7 +85,7 @@ MonoBehaviour:
|
||||
detachOnEnd: 1
|
||||
depositEffectPrefab: {fileID: 0}
|
||||
effectSpawnPoint: {fileID: 0}
|
||||
--- !u!114 &945062474581833766
|
||||
--- !u!114 &2964705630284685173
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
@@ -94,11 +94,22 @@ MonoBehaviour:
|
||||
m_GameObject: {fileID: 8124290768227340041}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: b59ae4328ce49c846b20d7a6d7ce7e47, type: 3}
|
||||
m_Script: {fileID: 11500000, guid: 0ceedb9b012d848478813136b65738ae, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Northbound.BuildingVisionProvider
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Northbound.Building
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
visionRange: 15
|
||||
buildingData: {fileID: 11400000, guid: 0e495d169ee3bce449f4b1aea83d6818, type: 2}
|
||||
gridPosition: {x: 0, y: 0, z: 0}
|
||||
rotation: 0
|
||||
initialOwnerId: 0
|
||||
useInitialOwner: 0
|
||||
showHealthBar: 1
|
||||
healthBarPrefab: {fileID: 0}
|
||||
destroyEffectPrefab: {fileID: 0}
|
||||
damageEffectPrefab: {fileID: 0}
|
||||
effectSpawnPoint: {fileID: 0}
|
||||
showGridBounds: 1
|
||||
gridBoundsColor: {r: 0, g: 1, b: 1, a: 1}
|
||||
--- !u!1001 &1876730568674182127
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@@ -20,7 +20,7 @@ GameObject:
|
||||
- component: {fileID: 6066313428661204362}
|
||||
- component: {fileID: 2443072964133329520}
|
||||
- component: {fileID: 2148255267416253297}
|
||||
m_Layer: 0
|
||||
m_Layer: 9
|
||||
m_Name: Player
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
@@ -83,6 +83,11 @@ MonoBehaviour:
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
moveSpeed: 5
|
||||
rotationSpeed: 10
|
||||
initialTeam: 1
|
||||
maxHealth: 100
|
||||
showHealthBar: 1
|
||||
damageEffectPrefab: {fileID: 0}
|
||||
deathEffectPrefab: {fileID: 0}
|
||||
--- !u!95 &1698609800605343773
|
||||
Animator:
|
||||
serializedVersion: 7
|
||||
@@ -265,6 +270,14 @@ PrefabInstance:
|
||||
serializedVersion: 3
|
||||
m_TransformParent: {fileID: 5887522270574905679}
|
||||
m_Modifications:
|
||||
- target: {fileID: -9217289772674400175, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8749280752073934791, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
value: 0
|
||||
@@ -309,6 +322,18 @@ PrefabInstance:
|
||||
propertyPath: 'm_Materials.Array.data[0]'
|
||||
value:
|
||||
objectReference: {fileID: 2100000, guid: d64c307f1b4197c44970c29f9845c245, type: 2}
|
||||
- target: {fileID: -8629495297202134608, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8343894014087287100, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -7440837640338354081, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -7164137249434462698, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: 'm_Materials.Array.data[0]'
|
||||
value:
|
||||
@@ -317,26 +342,138 @@ PrefabInstance:
|
||||
propertyPath: 'm_Materials.Array.data[0]'
|
||||
value:
|
||||
objectReference: {fileID: 2100000, guid: d64c307f1b4197c44970c29f9845c245, type: 2}
|
||||
- target: {fileID: -6645333679261498596, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -6415790494268509736, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -5821640607724269708, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -5489203338784653783, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -5235451391474362517, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -3771971891951071861, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -3230676936149971385, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -3087093466631822622, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -2565563256467093774, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -2268532608001192311, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -1697328484770790153, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: 'm_Materials.Array.data[0]'
|
||||
value:
|
||||
objectReference: {fileID: 2100000, guid: d64c307f1b4197c44970c29f9845c245, type: 2}
|
||||
- target: {fileID: -787705004876472881, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -540077757996713287, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: 'm_Materials.Array.data[0]'
|
||||
value:
|
||||
objectReference: {fileID: 2100000, guid: d64c307f1b4197c44970c29f9845c245, type: 2}
|
||||
- target: {fileID: -129758803659467788, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 58325350010635884, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 316027028415609013, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 919132149155446097, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Name
|
||||
value: Dummy
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 919132149155446097, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 2071513765411332684, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 2870523794981691266, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3424958915229829536, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4260319478689324092, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4700718891997078985, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4902866164562761394, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5737771032674114347, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: 'm_Materials.Array.data[0]'
|
||||
value:
|
||||
objectReference: {fileID: 2100000, guid: d64c307f1b4197c44970c29f9845c245, type: 2}
|
||||
- target: {fileID: 6026115103549348504, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6041330210177057777, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7116785954168059035, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7354008096416770832, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 8498165350272959449, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 8594629792745241546, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: 'm_Materials.Array.data[0]'
|
||||
value:
|
||||
objectReference: {fileID: 2100000, guid: d64c307f1b4197c44970c29f9845c245, type: 2}
|
||||
- target: {fileID: 8674666463418728774, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 9179509530285530577, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents: []
|
||||
m_RemovedGameObjects: []
|
||||
m_AddedGameObjects: []
|
||||
|
||||
@@ -75,9 +75,6 @@ PrefabInstance:
|
||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 244a8d70d41b1a948beb2221c7c0efa9, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 1591641544412467547}
|
||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 244a8d70d41b1a948beb2221c7c0efa9, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 1638952835164862066}
|
||||
m_SourcePrefab: {fileID: 100100000, guid: 244a8d70d41b1a948beb2221c7c0efa9, type: 3}
|
||||
--- !u!1 &2938167817760513538 stripped
|
||||
GameObject:
|
||||
@@ -96,7 +93,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
||||
GlobalObjectIdHash: 3026494903
|
||||
GlobalObjectIdHash: 2718147317
|
||||
InScenePlacedSourceGlobalObjectIdHash: 0
|
||||
DeferredDespawnTick: 0
|
||||
Ownership: 1
|
||||
@@ -109,17 +106,3 @@ MonoBehaviour:
|
||||
AutoObjectParentSync: 1
|
||||
SyncOwnerTransformWhenParented: 1
|
||||
AllowOwnerToParent: 0
|
||||
--- !u!114 &1638952835164862066
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2938167817760513538}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: b59ae4328ce49c846b20d7a6d7ce7e47, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Northbound.BuildingVisionProvider
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
visionRange: 15
|
||||
|
||||
8
Assets/Scenes/GameMain.meta
Normal file
8
Assets/Scenes/GameMain.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e8051163255a7b946abfc30e245d51c3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -543,6 +543,141 @@ Transform:
|
||||
m_CorrespondingSourceObject: {fileID: 922888705413710451, guid: 5662d0b0d0eb5f54290edd8dd0980b57, type: 3}
|
||||
m_PrefabInstance: {fileID: 2588157855179843872}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!1 &513701714
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 513701719}
|
||||
- component: {fileID: 513701718}
|
||||
- component: {fileID: 513701717}
|
||||
- component: {fileID: 513701716}
|
||||
- component: {fileID: 513701715}
|
||||
m_Layer: 0
|
||||
m_Name: EnemyTest
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &513701715
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 513701714}
|
||||
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: 1957460564
|
||||
InScenePlacedSourceGlobalObjectIdHash: 0
|
||||
DeferredDespawnTick: 0
|
||||
Ownership: 1
|
||||
AlwaysReplicateAsRoot: 0
|
||||
SynchronizeTransform: 1
|
||||
ActiveSceneSynchronization: 0
|
||||
SceneMigrationSynchronization: 0
|
||||
SpawnWithObservers: 1
|
||||
DontDestroyWithOwner: 0
|
||||
AutoObjectParentSync: 1
|
||||
SyncOwnerTransformWhenParented: 1
|
||||
AllowOwnerToParent: 0
|
||||
--- !u!114 &513701716
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 513701714}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 345fc6e7d4f06314f8b548129700eccb, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Northbound.EnemyUnit
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
enemyTeam: 2
|
||||
maxHealth: 100
|
||||
visionRange: 10
|
||||
damageEffectPrefab: {fileID: 0}
|
||||
destroyEffectPrefab: {fileID: 0}
|
||||
--- !u!23 &513701717
|
||||
MeshRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 513701714}
|
||||
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: e831e374758eb4a019b3031699e35407, 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 &513701718
|
||||
MeshFilter:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 513701714}
|
||||
m_Mesh: {fileID: 5840309848095958252, guid: a2ea40155b7314a559bca224f68394d6, type: 3}
|
||||
--- !u!4 &513701719
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 513701714}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 6, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &519420028
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -2338,7 +2473,10 @@ PrefabInstance:
|
||||
m_RemovedComponents: []
|
||||
m_RemovedGameObjects: []
|
||||
m_AddedGameObjects: []
|
||||
m_AddedComponents: []
|
||||
m_AddedComponents:
|
||||
- targetCorrespondingSourceObject: {fileID: 8124290768227340041, guid: e56926eda34629f4fbf3e4c53f0f8bd4, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 8940572951313384068}
|
||||
m_SourcePrefab: {fileID: 100100000, guid: e56926eda34629f4fbf3e4c53f0f8bd4, type: 3}
|
||||
--- !u!1001 &4875211098963642791
|
||||
PrefabInstance:
|
||||
@@ -2466,17 +2604,48 @@ PrefabInstance:
|
||||
m_AddedGameObjects: []
|
||||
m_AddedComponents: []
|
||||
m_SourcePrefab: {fileID: 100100000, guid: 04e95700704d92248b63ce5674bd9638, type: 3}
|
||||
--- !u!1 &8940572951313384064 stripped
|
||||
GameObject:
|
||||
m_CorrespondingSourceObject: {fileID: 8124290768227340041, guid: e56926eda34629f4fbf3e4c53f0f8bd4, type: 3}
|
||||
m_PrefabInstance: {fileID: 4786254629656932894}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!114 &8940572951313384066 stripped
|
||||
MonoBehaviour:
|
||||
m_CorrespondingSourceObject: {fileID: 1287070985890992582, guid: e56926eda34629f4fbf3e4c53f0f8bd4, type: 3}
|
||||
m_PrefabInstance: {fileID: 4786254629656932894}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_GameObject: {fileID: 8940572951313384064}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 7c94274e2af2c8d4f827fe52b26c4410, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Northbound.Core
|
||||
--- !u!114 &8940572951313384068
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8940572951313384064}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 0ceedb9b012d848478813136b65738ae, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Northbound.Building
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
buildingData: {fileID: 11400000, guid: 23c12a82ea534b34299700b86fffd524, type: 2}
|
||||
gridPosition: {x: 0, y: 0, z: 0}
|
||||
rotation: 0
|
||||
initialTeam: 1
|
||||
initialOwnerId: 0
|
||||
useInitialOwner: 0
|
||||
showHealthBar: 0
|
||||
healthBarPrefab: {fileID: 0}
|
||||
destroyEffectPrefab: {fileID: 0}
|
||||
damageEffectPrefab: {fileID: 0}
|
||||
effectSpawnPoint: {fileID: 0}
|
||||
showGridBounds: 1
|
||||
gridBoundsColor: {r: 0, g: 1, b: 1, a: 1}
|
||||
--- !u!1660057539 &9223372036854775807
|
||||
SceneRoots:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -2496,3 +2665,4 @@ SceneRoots:
|
||||
- {fileID: 1166878644}
|
||||
- {fileID: 946527919}
|
||||
- {fileID: 1701756768}
|
||||
- {fileID: 513701719}
|
||||
|
||||
BIN
Assets/Scenes/GameMain/NavMesh-Primitive_Floor.asset
Normal file
BIN
Assets/Scenes/GameMain/NavMesh-Primitive_Floor.asset
Normal file
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a847cf63b54abee4cbd5b6c92c8ad5e6
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -4,7 +4,7 @@ using UnityEngine;
|
||||
namespace Northbound
|
||||
{
|
||||
/// <summary>
|
||||
/// 액션 - 공격 (대상 없이도 실행 가능)
|
||||
/// 액션 - 공격 (팀 시스템 적용)
|
||||
/// </summary>
|
||||
public class AttackAction : NetworkBehaviour, IAction
|
||||
{
|
||||
@@ -15,7 +15,7 @@ namespace Northbound
|
||||
public LayerMask attackableLayer = ~0;
|
||||
|
||||
[Header("Animation")]
|
||||
public string attackAnimationTrigger = "Attack"; // 공격 애니메이션 트리거
|
||||
public string attackAnimationTrigger = "Attack";
|
||||
|
||||
[Header("Visual")]
|
||||
public GameObject attackEffectPrefab;
|
||||
@@ -23,10 +23,12 @@ namespace Northbound
|
||||
|
||||
private float _lastAttackTime;
|
||||
private Animator _animator;
|
||||
private ITeamMember _teamMember;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_animator = GetComponent<Animator>();
|
||||
_teamMember = GetComponent<ITeamMember>();
|
||||
}
|
||||
|
||||
public bool CanExecute(ulong playerId)
|
||||
@@ -44,7 +46,7 @@ namespace Northbound
|
||||
// 애니메이션 재생
|
||||
PlayAttackAnimation();
|
||||
|
||||
// 범위 내 적이 있으면 데미지
|
||||
// 범위 내 적 검색
|
||||
Vector3 attackOrigin = attackPoint != null ? attackPoint.position : transform.position;
|
||||
Collider[] hits = Physics.OverlapSphere(attackOrigin, attackRange, attackableLayer);
|
||||
|
||||
@@ -54,10 +56,22 @@ namespace Northbound
|
||||
if (hit.transform.root == transform.root)
|
||||
continue;
|
||||
|
||||
// 적에게 데미지
|
||||
var enemy = hit.GetComponent<IDamageable>();
|
||||
if (enemy != null)
|
||||
// 대상 확인
|
||||
var targetDamageable = hit.GetComponent<IDamageable>();
|
||||
var targetTeamMember = hit.GetComponent<ITeamMember>();
|
||||
|
||||
if (targetDamageable != null)
|
||||
{
|
||||
// 팀 확인 - 적대 관계인 경우에만 공격
|
||||
if (_teamMember != null && targetTeamMember != null)
|
||||
{
|
||||
if (!TeamManager.CanAttack(_teamMember, targetTeamMember))
|
||||
{
|
||||
Debug.Log($"<color=yellow>[AttackAction] {TeamManager.GetTeamName(_teamMember.GetTeam())} 팀은 {TeamManager.GetTeamName(targetTeamMember.GetTeam())} 팀을 공격할 수 없습니다.</color>");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var netObj = hit.GetComponent<NetworkObject>();
|
||||
if (netObj != null)
|
||||
{
|
||||
@@ -66,7 +80,7 @@ namespace Northbound
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"플레이어 {playerId} 공격! (적중: {hits.Length}개)");
|
||||
Debug.Log($"<color=cyan>[AttackAction] 플레이어 {playerId} ({TeamManager.GetTeamName(_teamMember?.GetTeam() ?? TeamType.Neutral)}) 공격!</color>");
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
|
||||
177
Assets/Scripts/AutoTargetSystem.cs
Normal file
177
Assets/Scripts/AutoTargetSystem.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound
|
||||
{
|
||||
/// <summary>
|
||||
/// 자동으로 적을 탐지하고 공격하는 시스템
|
||||
/// </summary>
|
||||
public class AutoTargetSystem : NetworkBehaviour
|
||||
{
|
||||
[Header("Targeting")]
|
||||
[Tooltip("적을 감지하는 범위")]
|
||||
public float detectionRange = 15f;
|
||||
|
||||
[Tooltip("공격 가능한 범위")]
|
||||
public float attackRange = 10f;
|
||||
|
||||
[Tooltip("공격 간격 (초)")]
|
||||
public float attackInterval = 1f;
|
||||
|
||||
[Tooltip("탐지할 레이어")]
|
||||
public LayerMask targetLayer = ~0;
|
||||
|
||||
[Header("Combat")]
|
||||
[Tooltip("공격 데미지")]
|
||||
public int attackDamage = 10;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("디버그 정보 표시")]
|
||||
public bool showDebugInfo = true;
|
||||
|
||||
private ITeamMember _teamMember;
|
||||
private float _lastAttackTime;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_teamMember = GetComponent<ITeamMember>();
|
||||
|
||||
if (_teamMember == null)
|
||||
{
|
||||
Debug.LogError($"<color=red>[AutoTargetSystem] {gameObject.name}에 ITeamMember 컴포넌트가 없습니다!</color>");
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsServer) return;
|
||||
if (_teamMember == null) return;
|
||||
|
||||
if (Time.time - _lastAttackTime >= attackInterval)
|
||||
{
|
||||
FindAndAttackEnemy();
|
||||
}
|
||||
}
|
||||
|
||||
private void FindAndAttackEnemy()
|
||||
{
|
||||
// 범위 내 모든 콜라이더 탐지
|
||||
Collider[] colliders = Physics.OverlapSphere(transform.position, detectionRange, targetLayer);
|
||||
|
||||
if (showDebugInfo && colliders.Length > 0)
|
||||
{
|
||||
Debug.Log($"<color=cyan>[AutoTarget] {gameObject.name}이(가) {colliders.Length}개의 오브젝트를 감지했습니다.</color>");
|
||||
}
|
||||
|
||||
GameObject closestEnemy = null;
|
||||
float closestDistance = float.MaxValue;
|
||||
|
||||
foreach (Collider col in colliders)
|
||||
{
|
||||
// 자기 자신 제외
|
||||
if (col.transform.root == transform.root)
|
||||
continue;
|
||||
|
||||
// 팀 확인
|
||||
ITeamMember targetTeam = col.GetComponent<ITeamMember>();
|
||||
|
||||
if (targetTeam == null)
|
||||
{
|
||||
// 부모나 자식에서 찾기
|
||||
targetTeam = col.GetComponentInParent<ITeamMember>();
|
||||
if (targetTeam == null)
|
||||
{
|
||||
targetTeam = col.GetComponentInChildren<ITeamMember>();
|
||||
}
|
||||
}
|
||||
|
||||
if (targetTeam == null)
|
||||
{
|
||||
if (showDebugInfo)
|
||||
{
|
||||
Debug.Log($"<color=yellow>[AutoTarget] {col.gameObject.name}에 ITeamMember가 없습니다.</color>");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 적대 관계 확인
|
||||
bool canAttack = TeamManager.CanAttack(_teamMember, targetTeam);
|
||||
|
||||
if (showDebugInfo)
|
||||
{
|
||||
Debug.Log($"<color=yellow>[AutoTarget] {gameObject.name} ({TeamManager.GetTeamName(_teamMember.GetTeam())}) → {col.gameObject.name} ({TeamManager.GetTeamName(targetTeam.GetTeam())}): 공격가능={canAttack}</color>");
|
||||
}
|
||||
|
||||
if (!canAttack)
|
||||
continue;
|
||||
|
||||
// 가장 가까운 적 찾기
|
||||
float distance = Vector3.Distance(transform.position, col.transform.position);
|
||||
if (distance < closestDistance && distance <= attackRange)
|
||||
{
|
||||
closestDistance = distance;
|
||||
closestEnemy = col.gameObject;
|
||||
}
|
||||
}
|
||||
|
||||
// 공격
|
||||
if (closestEnemy != null)
|
||||
{
|
||||
IDamageable damageable = closestEnemy.GetComponent<IDamageable>();
|
||||
|
||||
if (damageable == null)
|
||||
{
|
||||
damageable = closestEnemy.GetComponentInParent<IDamageable>();
|
||||
if (damageable == null)
|
||||
{
|
||||
damageable = closestEnemy.GetComponentInChildren<IDamageable>();
|
||||
}
|
||||
}
|
||||
|
||||
if (damageable != null)
|
||||
{
|
||||
damageable.TakeDamage(attackDamage, NetworkObjectId);
|
||||
_lastAttackTime = Time.time;
|
||||
|
||||
var targetTeam = closestEnemy.GetComponent<ITeamMember>() ??
|
||||
closestEnemy.GetComponentInParent<ITeamMember>() ??
|
||||
closestEnemy.GetComponentInChildren<ITeamMember>();
|
||||
|
||||
Debug.Log($"<color=red>[AutoTarget] {gameObject.name} ({TeamManager.GetTeamName(_teamMember.GetTeam())})이(가) {closestEnemy.name} ({TeamManager.GetTeamName(targetTeam?.GetTeam() ?? TeamType.Neutral)})을(를) 공격! (거리: {closestDistance:F2}m, 데미지: {attackDamage})</color>");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"<color=orange>[AutoTarget] {closestEnemy.name}에 IDamageable이 없습니다.</color>");
|
||||
}
|
||||
}
|
||||
else if (showDebugInfo && colliders.Length > 0)
|
||||
{
|
||||
Debug.Log($"<color=yellow>[AutoTarget] {gameObject.name}이(가) 공격 가능한 적을 찾지 못했습니다.</color>");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
// 탐지 범위 (노란색)
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawWireSphere(transform.position, detectionRange);
|
||||
|
||||
// 공격 범위 (빨간색)
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(transform.position, attackRange);
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
OnDrawGizmos();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (_teamMember != null && Application.isPlaying)
|
||||
{
|
||||
UnityEditor.Handles.Label(transform.position + Vector3.up * 3f,
|
||||
$"Auto Target\nTeam: {TeamManager.GetTeamName(_teamMember.GetTeam())}\nDetection: {detectionRange}m\nAttack: {attackRange}m");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/AutoTargetSystem.cs.meta
Normal file
2
Assets/Scripts/AutoTargetSystem.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8dd1c341faa09554aa2bc35164888453
|
||||
@@ -4,7 +4,7 @@ using UnityEngine;
|
||||
|
||||
namespace Northbound
|
||||
{
|
||||
public class Building : NetworkBehaviour, IDamageable, IVisionProvider
|
||||
public class Building : NetworkBehaviour, IDamageable, IVisionProvider, ITeamMember
|
||||
{
|
||||
[Header("References")]
|
||||
public BuildingData buildingData;
|
||||
@@ -13,6 +13,10 @@ namespace Northbound
|
||||
public Vector3Int gridPosition;
|
||||
public int rotation; // 0-3 (0=0°, 1=90°, 2=180°, 3=270°)
|
||||
|
||||
[Header("Team")]
|
||||
[Tooltip("건물의 팀 (플레이어/적대세력/몬스터/중립)")]
|
||||
public TeamType initialTeam = TeamType.Player;
|
||||
|
||||
[Header("Ownership (for pre-placed buildings)")]
|
||||
[Tooltip("씬에 미리 배치된 건물의 경우 여기서 소유자 설정 (0 = 중립, 1+ = 플레이어 ID)")]
|
||||
public ulong initialOwnerId = 0;
|
||||
@@ -46,9 +50,17 @@ namespace Northbound
|
||||
NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
// 건물 팀
|
||||
private NetworkVariable<TeamType> _team = new NetworkVariable<TeamType>(
|
||||
TeamType.Neutral,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
// 이벤트
|
||||
public event Action<int, int> OnHealthChanged; // (current, max)
|
||||
public event Action OnDestroyed;
|
||||
public event Action<TeamType> OnTeamChanged;
|
||||
|
||||
private BuildingHealthBar _healthBar;
|
||||
private float _lastRegenTime;
|
||||
@@ -66,11 +78,17 @@ namespace Northbound
|
||||
_currentHealth.Value = buildingData != null ? buildingData.maxHealth : 100;
|
||||
}
|
||||
|
||||
// 팀 초기화
|
||||
if (_team.Value == TeamType.Neutral)
|
||||
{
|
||||
_team.Value = initialTeam;
|
||||
}
|
||||
|
||||
// 소유자 초기화 (사전 배치 건물 체크)
|
||||
if (useInitialOwner && _ownerId.Value == 0)
|
||||
{
|
||||
_ownerId.Value = initialOwnerId;
|
||||
Debug.Log($"<color=cyan>[Building] 사전 배치 건물 '{buildingData?.buildingName ?? gameObject.name}' 소유자: {initialOwnerId}</color>");
|
||||
Debug.Log($"<color=cyan>[Building] 사전 배치 건물 '{buildingData?.buildingName ?? gameObject.name}' 소유자: {initialOwnerId}, 팀: {_team.Value}</color>");
|
||||
}
|
||||
else if (!useInitialOwner && _ownerId.Value == 0)
|
||||
{
|
||||
@@ -87,8 +105,9 @@ namespace Northbound
|
||||
}
|
||||
}
|
||||
|
||||
// 체력 변경 이벤트 구독
|
||||
// 이벤트 구독
|
||||
_currentHealth.OnValueChanged += OnHealthValueChanged;
|
||||
_team.OnValueChanged += OnTeamValueChanged;
|
||||
|
||||
// 체력바 생성
|
||||
if (showHealthBar && healthBarPrefab != null)
|
||||
@@ -98,11 +117,13 @@ namespace Northbound
|
||||
|
||||
// 초기 체력 UI 업데이트
|
||||
UpdateHealthUI();
|
||||
UpdateTeamVisuals();
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
_currentHealth.OnValueChanged -= OnHealthValueChanged;
|
||||
_team.OnValueChanged -= OnTeamValueChanged;
|
||||
|
||||
// FogOfWar 시스템에서 제거
|
||||
if (IsServer && buildingData != null && buildingData.providesVision)
|
||||
@@ -131,7 +152,7 @@ namespace Northbound
|
||||
/// <summary>
|
||||
/// 건물 초기화 (BuildingManager가 동적 생성 시 호출)
|
||||
/// </summary>
|
||||
public void Initialize(BuildingData data, Vector3Int gridPos, int rot, ulong ownerId)
|
||||
public void Initialize(BuildingData data, Vector3Int gridPos, int rot, ulong ownerId, TeamType team = TeamType.Player)
|
||||
{
|
||||
buildingData = data;
|
||||
gridPosition = gridPos;
|
||||
@@ -142,6 +163,7 @@ namespace Northbound
|
||||
{
|
||||
_currentHealth.Value = data.maxHealth;
|
||||
_ownerId.Value = ownerId;
|
||||
_team.Value = team;
|
||||
|
||||
// 시야 제공자 등록
|
||||
if (data.providesVision)
|
||||
@@ -156,14 +178,17 @@ namespace Northbound
|
||||
/// <summary>
|
||||
/// 건물 소유권 변경 (점령 등)
|
||||
/// </summary>
|
||||
public void SetOwner(ulong newOwnerId)
|
||||
public void SetOwner(ulong newOwnerId, TeamType newTeam)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
ulong previousOwner = _ownerId.Value;
|
||||
TeamType previousTeam = _team.Value;
|
||||
|
||||
_ownerId.Value = newOwnerId;
|
||||
_team.Value = newTeam;
|
||||
|
||||
Debug.Log($"<color=yellow>[Building] {buildingData?.buildingName ?? "건물"} 소유권 변경: {previousOwner} → {newOwnerId}</color>");
|
||||
Debug.Log($"<color=yellow>[Building] {buildingData?.buildingName ?? "건물"} 소유권 변경: {previousOwner} → {newOwnerId}, 팀: {TeamManager.GetTeamName(previousTeam)} → {TeamManager.GetTeamName(newTeam)}</color>");
|
||||
|
||||
// 시야 제공자 재등록 (소유자가 바뀌었으므로)
|
||||
if (buildingData != null && buildingData.providesVision)
|
||||
@@ -173,6 +198,35 @@ namespace Northbound
|
||||
}
|
||||
}
|
||||
|
||||
#region ITeamMember Implementation
|
||||
|
||||
public TeamType GetTeam() => _team.Value;
|
||||
|
||||
public void SetTeam(TeamType team)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
_team.Value = team;
|
||||
}
|
||||
|
||||
private void OnTeamValueChanged(TeamType previousValue, TeamType newValue)
|
||||
{
|
||||
OnTeamChanged?.Invoke(newValue);
|
||||
UpdateTeamVisuals();
|
||||
Debug.Log($"<color=cyan>[Building] {buildingData?.buildingName ?? "건물"} 팀 변경: {TeamManager.GetTeamName(previousValue)} → {TeamManager.GetTeamName(newValue)}</color>");
|
||||
}
|
||||
|
||||
private void UpdateTeamVisuals()
|
||||
{
|
||||
// 팀 색상으로 건물 외곽선이나 이펙트 변경 가능
|
||||
// 예: Renderer의 emission 색상 변경
|
||||
Color teamColor = TeamManager.GetTeamColor(_team.Value);
|
||||
|
||||
// 여기에 실제 비주얼 업데이트 로직 추가
|
||||
// 예: outline shader, emission, particle system 색상 등
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IVisionProvider Implementation
|
||||
|
||||
public ulong GetOwnerId() => _ownerId.Value;
|
||||
@@ -214,11 +268,24 @@ namespace Northbound
|
||||
if (_currentHealth.Value <= 0)
|
||||
return;
|
||||
|
||||
// 공격자의 팀 확인 (팀 공격 방지)
|
||||
var attackerObj = NetworkManager.Singleton.SpawnManager.SpawnedObjects[attackerId];
|
||||
var attackerTeamMember = attackerObj?.GetComponent<ITeamMember>();
|
||||
|
||||
if (attackerTeamMember != null)
|
||||
{
|
||||
if (!TeamManager.CanAttack(attackerTeamMember, this))
|
||||
{
|
||||
Debug.Log($"<color=yellow>[Building] {TeamManager.GetTeamName(attackerTeamMember.GetTeam())} 팀은 {TeamManager.GetTeamName(_team.Value)} 팀을 공격할 수 없습니다.</color>");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 데미지 적용
|
||||
int actualDamage = Mathf.Min(damage, _currentHealth.Value);
|
||||
_currentHealth.Value -= actualDamage;
|
||||
|
||||
Debug.Log($"<color=red>[Building] {buildingData?.buildingName ?? "건물"}이(가) {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{buildingData?.maxHealth ?? 100}</color>");
|
||||
Debug.Log($"<color=red>[Building] {buildingData?.buildingName ?? "건물"} ({TeamManager.GetTeamName(_team.Value)})이(가) {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{buildingData?.maxHealth ?? 100}</color>");
|
||||
|
||||
// 데미지 이펙트
|
||||
ShowDamageEffectClientRpc();
|
||||
@@ -248,7 +315,7 @@ namespace Northbound
|
||||
if (!IsServer)
|
||||
return;
|
||||
|
||||
Debug.Log($"<color=red>[Building] {buildingData?.buildingName ?? "건물"}이(가) 파괴되었습니다! (공격자: {attackerId})</color>");
|
||||
Debug.Log($"<color=red>[Building] {buildingData?.buildingName ?? "건물"} ({TeamManager.GetTeamName(_team.Value)})이(가) 파괴되었습니다! (공격자: {attackerId})</color>");
|
||||
|
||||
// 파괴 이벤트 발생
|
||||
OnDestroyed?.Invoke();
|
||||
@@ -427,7 +494,10 @@ namespace Northbound
|
||||
if (!showGridBounds || buildingData == null) return;
|
||||
|
||||
Bounds bounds = GetGridBounds();
|
||||
Gizmos.color = gridBoundsColor;
|
||||
|
||||
// 팀 색상으로 표시
|
||||
Color teamColor = Application.isPlaying ? TeamManager.GetTeamColor(_team.Value) : TeamManager.GetTeamColor(initialTeam);
|
||||
Gizmos.color = new Color(teamColor.r, teamColor.g, teamColor.b, 0.3f);
|
||||
Gizmos.DrawWireCube(bounds.center, bounds.size);
|
||||
}
|
||||
|
||||
@@ -454,17 +524,19 @@ namespace Northbound
|
||||
Gizmos.DrawWireSphere(transform.position, buildingData.visionRange);
|
||||
}
|
||||
|
||||
// Draw owner ID label
|
||||
// Draw team info label
|
||||
#if UNITY_EDITOR
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
string teamName = TeamManager.GetTeamName(_team.Value);
|
||||
UnityEditor.Handles.Label(transform.position + Vector3.up * 3f,
|
||||
$"Owner: {_ownerId.Value}");
|
||||
$"Owner: {_ownerId.Value}\nTeam: {teamName}");
|
||||
}
|
||||
else if (useInitialOwner)
|
||||
{
|
||||
string teamName = TeamManager.GetTeamName(initialTeam);
|
||||
UnityEditor.Handles.Label(transform.position + Vector3.up * 3f,
|
||||
$"Initial Owner: {initialOwnerId}");
|
||||
$"Initial Owner: {initialOwnerId}\nTeam: {teamName}");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -6,12 +6,17 @@ namespace Northbound
|
||||
/// <summary>
|
||||
/// 플레이어가 자원을 건내받아 게임의 전역 자원으로 관리하는 중앙 허브
|
||||
/// </summary>
|
||||
public class Core : NetworkBehaviour, IInteractable
|
||||
public class Core : NetworkBehaviour, IInteractable, IDamageable, ITeamMember
|
||||
{
|
||||
[Header("Core Settings")]
|
||||
public int maxStorageCapacity = 1000; // 코어의 최대 저장 용량
|
||||
public bool unlimitedStorage = false; // 무제한 저장소
|
||||
|
||||
[Header("Health")]
|
||||
public int maxHealth = 1000;
|
||||
public GameObject damageEffectPrefab;
|
||||
public GameObject destroyEffectPrefab;
|
||||
|
||||
[Header("Deposit Settings")]
|
||||
public bool depositAll = true; // true: 전부 건네기, false: 일부만 건네기
|
||||
public int depositAmountPerInteraction = 10; // depositAll이 false일 때 한 번에 건네는 양
|
||||
@@ -32,17 +37,163 @@ namespace Northbound
|
||||
NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
private NetworkVariable<int> _currentHealth = new NetworkVariable<int>(
|
||||
0,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
public int TotalResources => _totalResources.Value;
|
||||
public int MaxStorageCapacity => maxStorageCapacity;
|
||||
public int CurrentHealth => _currentHealth.Value;
|
||||
public int MaxHealth => maxHealth;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
if (IsServer)
|
||||
{
|
||||
_totalResources.Value = 0;
|
||||
_currentHealth.Value = maxHealth;
|
||||
}
|
||||
|
||||
_currentHealth.OnValueChanged += OnHealthChanged;
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
_currentHealth.OnValueChanged -= OnHealthChanged;
|
||||
}
|
||||
|
||||
private void OnHealthChanged(int previousValue, int newValue)
|
||||
{
|
||||
Debug.Log($"<color=red>[Core] 코어 체력 변경: {previousValue} → {newValue} ({newValue}/{maxHealth})</color>");
|
||||
}
|
||||
|
||||
#region ITeamMember Implementation
|
||||
|
||||
public TeamType GetTeam()
|
||||
{
|
||||
return TeamType.Player; // 코어는 플레이어 팀
|
||||
}
|
||||
|
||||
public void SetTeam(TeamType team)
|
||||
{
|
||||
// 코어의 팀은 변경할 수 없음 (항상 플레이어 팀)
|
||||
Debug.LogWarning("[Core] 코어의 팀은 변경할 수 없습니다.");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDamageable Implementation
|
||||
|
||||
public void TakeDamage(int damage, ulong attackerId)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
if (_currentHealth.Value <= 0) return;
|
||||
|
||||
int actualDamage = Mathf.Min(damage, _currentHealth.Value);
|
||||
_currentHealth.Value -= actualDamage;
|
||||
|
||||
Debug.Log($"<color=red>[Core] 코어가 {actualDamage} 데미지를 받았습니다! 남은 체력: {_currentHealth.Value}/{maxHealth}</color>");
|
||||
|
||||
// 데미지 이펙트
|
||||
ShowDamageEffectClientRpc();
|
||||
|
||||
// 체력이 0이 되면 게임 오버
|
||||
if (_currentHealth.Value <= 0)
|
||||
{
|
||||
OnCoreDestroyed();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCoreDestroyed()
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
Debug.Log($"<color=red>[Core] 코어가 파괴되었습니다! 게임 오버!</color>");
|
||||
|
||||
// 파괴 이펙트
|
||||
ShowDestroyEffectClientRpc();
|
||||
|
||||
// 게임 오버 로직 (추후 구현)
|
||||
// GameManager.Instance?.OnGameOver();
|
||||
}
|
||||
|
||||
[Rpc(SendTo.ClientsAndHost)]
|
||||
private void ShowDamageEffectClientRpc()
|
||||
{
|
||||
if (damageEffectPrefab != null)
|
||||
{
|
||||
GameObject effect = Instantiate(damageEffectPrefab, transform.position + Vector3.up * 2f, Quaternion.identity);
|
||||
Destroy(effect, 2f);
|
||||
}
|
||||
}
|
||||
|
||||
[Rpc(SendTo.ClientsAndHost)]
|
||||
private void ShowDestroyEffectClientRpc()
|
||||
{
|
||||
if (destroyEffectPrefab != null)
|
||||
{
|
||||
GameObject effect = Instantiate(destroyEffectPrefab, transform.position, Quaternion.identity);
|
||||
Destroy(effect, 5f);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Resource Management
|
||||
|
||||
/// <summary>
|
||||
/// 자원을 소비할 수 있는지 확인
|
||||
/// </summary>
|
||||
public bool CanConsumeResource(int amount)
|
||||
{
|
||||
return _totalResources.Value >= amount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 자원 소비 (서버에서만)
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
public void ConsumeResourceServerRpc(int amount)
|
||||
{
|
||||
if (!CanConsumeResource(amount))
|
||||
{
|
||||
Debug.LogWarning($"[Core] 자원이 부족합니다. 필요: {amount}, 보유: {_totalResources.Value}");
|
||||
return;
|
||||
}
|
||||
|
||||
int previousAmount = _totalResources.Value;
|
||||
_totalResources.Value -= amount;
|
||||
|
||||
Debug.Log($"<color=yellow>[Core] {amount} 자원 소비. 남은 자원: {_totalResources.Value}/{maxStorageCapacity}</color>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 자원 추가 (서버에서만)
|
||||
/// </summary>
|
||||
public void AddResource(int amount)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
if (!unlimitedStorage)
|
||||
{
|
||||
int availableSpace = maxStorageCapacity - _totalResources.Value;
|
||||
amount = Mathf.Min(amount, availableSpace);
|
||||
}
|
||||
|
||||
if (amount > 0)
|
||||
{
|
||||
_totalResources.Value += amount;
|
||||
Debug.Log($"<color=green>[Core] {amount} 자원 추가. 총 자원: {_totalResources.Value}" +
|
||||
(unlimitedStorage ? "" : $"/{maxStorageCapacity}") + "</color>");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IInteractable Implementation
|
||||
|
||||
public bool CanInteract(ulong playerId)
|
||||
{
|
||||
// 저장소가 가득 찼는지 확인 (무제한이 아닐 때)
|
||||
@@ -75,6 +226,33 @@ namespace Northbound
|
||||
DepositResourceServerRpc(playerId);
|
||||
}
|
||||
|
||||
public string GetInteractionPrompt()
|
||||
{
|
||||
if (unlimitedStorage)
|
||||
{
|
||||
return "자원 보관 (무제한)";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"자원 보관 ({_totalResources.Value}/{maxStorageCapacity})";
|
||||
}
|
||||
}
|
||||
|
||||
public string GetInteractionAnimation()
|
||||
{
|
||||
return interactionAnimationTrigger;
|
||||
}
|
||||
|
||||
public InteractionEquipmentData GetEquipmentData()
|
||||
{
|
||||
return equipmentData;
|
||||
}
|
||||
|
||||
public Transform GetTransform()
|
||||
{
|
||||
return transform;
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void DepositResourceServerRpc(ulong playerId)
|
||||
{
|
||||
@@ -132,8 +310,8 @@ namespace Northbound
|
||||
// 코어에 자원 추가
|
||||
_totalResources.Value += depositAmount;
|
||||
|
||||
Debug.Log($"플레이어 {playerId}가 {depositAmount} 자원을 코어에 건넸습니다. 코어 총 자원: {_totalResources.Value}" +
|
||||
(unlimitedStorage ? "" : $"/{maxStorageCapacity}"));
|
||||
Debug.Log($"<color=green>[Core] 플레이어 {playerId}가 {depositAmount} 자원을 건넸습니다. 코어 총 자원: {_totalResources.Value}" +
|
||||
(unlimitedStorage ? "" : $"/{maxStorageCapacity}") + "</color>");
|
||||
|
||||
ShowDepositEffectClientRpc();
|
||||
}
|
||||
@@ -148,76 +326,6 @@ namespace Northbound
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 게임 시스템이 코어의 자원을 사용 (건물 건설 등)
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
public void ConsumeResourceServerRpc(int amount)
|
||||
{
|
||||
if (amount <= 0) return;
|
||||
|
||||
int actualAmount = Mathf.Min(amount, _totalResources.Value);
|
||||
_totalResources.Value -= actualAmount;
|
||||
|
||||
Debug.Log($"코어에서 {actualAmount} 자원을 사용했습니다. 남은 자원: {_totalResources.Value}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 자원을 사용할 수 있는지 확인
|
||||
/// </summary>
|
||||
public bool CanConsumeResource(int amount)
|
||||
{
|
||||
return _totalResources.Value >= amount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 코어에 자원 추가 (디버그/관리자 기능)
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
public void AddResourceServerRpc(int amount)
|
||||
{
|
||||
if (amount <= 0) return;
|
||||
|
||||
if (!unlimitedStorage)
|
||||
{
|
||||
int availableSpace = maxStorageCapacity - _totalResources.Value;
|
||||
amount = Mathf.Min(amount, availableSpace);
|
||||
}
|
||||
|
||||
_totalResources.Value += amount;
|
||||
Debug.Log($"코어에 {amount} 자원이 추가되었습니다. 현재: {_totalResources.Value}");
|
||||
}
|
||||
|
||||
public string GetInteractionPrompt()
|
||||
{
|
||||
if (unlimitedStorage)
|
||||
{
|
||||
return depositAll ?
|
||||
$"[E] 자원 모두 건네기" :
|
||||
$"[E] 자원 건네기 ({depositAmountPerInteraction}개씩)";
|
||||
}
|
||||
|
||||
if (_totalResources.Value >= maxStorageCapacity)
|
||||
return "코어 저장소 가득 찼음";
|
||||
|
||||
return depositAll ?
|
||||
$"[E] 자원 모두 건네기 ({_totalResources.Value}/{maxStorageCapacity})" :
|
||||
$"[E] 자원 건네기 ({_totalResources.Value}/{maxStorageCapacity})";
|
||||
}
|
||||
|
||||
public string GetInteractionAnimation()
|
||||
{
|
||||
return interactionAnimationTrigger;
|
||||
}
|
||||
|
||||
public InteractionEquipmentData GetEquipmentData()
|
||||
{
|
||||
return equipmentData;
|
||||
}
|
||||
|
||||
public Transform GetTransform()
|
||||
{
|
||||
return transform;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
714
Assets/Scripts/EnemyAIController.cs
Normal file
714
Assets/Scripts/EnemyAIController.cs
Normal file
@@ -0,0 +1,714 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
|
||||
namespace Northbound
|
||||
{
|
||||
/// <summary>
|
||||
/// 몬스터와 적대 세력의 AI 컨트롤러
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(NavMeshAgent))]
|
||||
[RequireComponent(typeof(EnemyUnit))]
|
||||
public class EnemyAIController : NetworkBehaviour
|
||||
{
|
||||
[Header("AI Type")]
|
||||
[Tooltip("Monster: 코어로 이동, Hostile: 제자리에서 대기")]
|
||||
public TeamType aiType = TeamType.Monster;
|
||||
|
||||
[Header("Detection")]
|
||||
[Tooltip("플레이어 감지 범위")]
|
||||
public float detectionRange = 15f;
|
||||
|
||||
[Tooltip("시야 각도 (0-360, 360=전방향)")]
|
||||
[Range(0, 360)]
|
||||
public float detectionAngle = 120f;
|
||||
|
||||
[Tooltip("탐지할 레이어")]
|
||||
public LayerMask playerLayer = ~0;
|
||||
|
||||
[Tooltip("시야 체크 장애물 레이어")]
|
||||
public LayerMask obstacleLayer = ~0;
|
||||
|
||||
[Header("Chase Settings")]
|
||||
[Tooltip("추적 최대 거리 (이 거리 이상 추적하면 중단)")]
|
||||
public float maxChaseDistance = 30f;
|
||||
|
||||
[Tooltip("추적 포기 거리 (플레이어와 이 거리 이상 멀어지면 추적 중단)")]
|
||||
public float chaseGiveUpDistance = 25f;
|
||||
|
||||
[Header("Combat")]
|
||||
[Tooltip("공격 범위")]
|
||||
public float attackRange = 2f;
|
||||
|
||||
[Tooltip("공격 간격 (초)")]
|
||||
public float attackInterval = 1.5f;
|
||||
|
||||
[Tooltip("공격 데미지")]
|
||||
public int attackDamage = 10;
|
||||
|
||||
[Header("Movement")]
|
||||
[Tooltip("이동 속도")]
|
||||
public float moveSpeed = 3.5f;
|
||||
|
||||
[Tooltip("추적 중 속도 배율")]
|
||||
public float chaseSpeedMultiplier = 1.5f;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("디버그 정보 표시")]
|
||||
public bool showDebugInfo = true;
|
||||
|
||||
private NavMeshAgent _agent;
|
||||
private EnemyUnit _enemyUnit;
|
||||
private Transform _coreTransform;
|
||||
private Collider _coreCollider;
|
||||
private Vector3 _originPosition;
|
||||
private Vector3 _chaseStartPosition;
|
||||
private float _lastAttackTime;
|
||||
private bool _hasSetCoreDestination;
|
||||
private float _lastDetectionLogTime;
|
||||
|
||||
private NetworkVariable<EnemyAIState> _currentState = new NetworkVariable<EnemyAIState>(
|
||||
EnemyAIState.Idle,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
private NetworkVariable<ulong> _targetPlayerId = new NetworkVariable<ulong>(
|
||||
0,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
private GameObject _cachedTargetPlayer;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
base.OnNetworkSpawn();
|
||||
|
||||
_agent = GetComponent<NavMeshAgent>();
|
||||
_enemyUnit = GetComponent<EnemyUnit>();
|
||||
_originPosition = transform.position;
|
||||
|
||||
if (IsServer)
|
||||
{
|
||||
// NavMeshAgent 초기 설정
|
||||
_agent.speed = moveSpeed;
|
||||
_agent.acceleration = 8f;
|
||||
_agent.angularSpeed = 120f;
|
||||
_agent.stoppingDistance = attackRange * 0.7f;
|
||||
_agent.autoBraking = true;
|
||||
_agent.updateRotation = true;
|
||||
_agent.updateUpAxis = false;
|
||||
|
||||
// NavMesh 위에 있는지 확인
|
||||
if (!_agent.isOnNavMesh)
|
||||
{
|
||||
Debug.LogWarning($"<color=orange>[EnemyAI] {gameObject.name}이(가) NavMesh 위에 있지 않습니다!</color>");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"<color=green>[EnemyAI] {gameObject.name} NavMeshAgent 초기화 완료</color>");
|
||||
}
|
||||
|
||||
// AI 타입에 따라 초기 상태 설정
|
||||
if (aiType == TeamType.Monster)
|
||||
{
|
||||
FindCore();
|
||||
TransitionToState(EnemyAIState.MoveToCore);
|
||||
}
|
||||
else if (aiType == TeamType.Hostile)
|
||||
{
|
||||
TransitionToState(EnemyAIState.Idle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsServer) return;
|
||||
if (!_agent.isOnNavMesh) return;
|
||||
|
||||
switch (_currentState.Value)
|
||||
{
|
||||
case EnemyAIState.Idle:
|
||||
UpdateIdle();
|
||||
break;
|
||||
case EnemyAIState.MoveToCore:
|
||||
UpdateMoveToCore();
|
||||
break;
|
||||
case EnemyAIState.ChasePlayer:
|
||||
UpdateChasePlayer();
|
||||
break;
|
||||
case EnemyAIState.Attack:
|
||||
UpdateAttack();
|
||||
break;
|
||||
case EnemyAIState.ReturnToOrigin:
|
||||
UpdateReturnToOrigin();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#region State Updates
|
||||
|
||||
private void UpdateIdle()
|
||||
{
|
||||
GameObject player = DetectPlayer();
|
||||
if (player != null)
|
||||
{
|
||||
SetTargetPlayer(player);
|
||||
TransitionToState(EnemyAIState.ChasePlayer);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateMoveToCore()
|
||||
{
|
||||
// 플레이어 감지
|
||||
GameObject player = DetectPlayer();
|
||||
if (player != null)
|
||||
{
|
||||
SetTargetPlayer(player);
|
||||
TransitionToState(EnemyAIState.ChasePlayer);
|
||||
return;
|
||||
}
|
||||
|
||||
// 코어가 없으면 찾기
|
||||
if (_coreTransform == null)
|
||||
{
|
||||
FindCore();
|
||||
_hasSetCoreDestination = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 코어 표면까지의 실제 거리 계산
|
||||
float distanceToCore = GetDistanceToCoreSurface();
|
||||
|
||||
// 공격 범위 안에 있으면 공격 상태로 전환
|
||||
if (distanceToCore <= attackRange)
|
||||
{
|
||||
TransitionToState(EnemyAIState.Attack);
|
||||
return;
|
||||
}
|
||||
|
||||
// 경로가 설정되지 않았거나 무효화된 경우에만 설정
|
||||
if (!_hasSetCoreDestination || !_agent.hasPath || _agent.pathStatus == NavMeshPathStatus.PathInvalid)
|
||||
{
|
||||
if (_agent.SetDestination(_coreTransform.position))
|
||||
{
|
||||
_hasSetCoreDestination = true;
|
||||
|
||||
if (showDebugInfo)
|
||||
{
|
||||
Debug.Log($"<color=cyan>[EnemyAI] {gameObject.name} 코어로 경로 설정 (표면 거리: {distanceToCore:F2}m, 공격범위: {attackRange:F2}m)</color>");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"<color=orange>[EnemyAI] {gameObject.name}이(가) 코어로 가는 경로를 찾을 수 없습니다!</color>");
|
||||
_hasSetCoreDestination = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateChasePlayer()
|
||||
{
|
||||
GameObject targetPlayer = GetTargetPlayer();
|
||||
|
||||
if (targetPlayer == null)
|
||||
{
|
||||
OnLostTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
float distanceToPlayer = Vector3.Distance(transform.position, targetPlayer.transform.position);
|
||||
|
||||
// 추적 기준점 설정
|
||||
Vector3 chaseReferencePoint = (aiType == TeamType.Monster) ? _chaseStartPosition : _originPosition;
|
||||
float distanceFromReference = Vector3.Distance(transform.position, chaseReferencePoint);
|
||||
|
||||
// 추적 중단 조건 확인
|
||||
if (distanceToPlayer > chaseGiveUpDistance || distanceFromReference > maxChaseDistance)
|
||||
{
|
||||
if (showDebugInfo)
|
||||
{
|
||||
string referenceType = (aiType == TeamType.Monster) ? "추적 시작" : "원점";
|
||||
Debug.Log($"<color=yellow>[EnemyAI] {gameObject.name}이(가) 추적을 중단합니다. (플레이어 거리: {distanceToPlayer:F2}m, {referenceType} 거리: {distanceFromReference:F2}m)</color>");
|
||||
}
|
||||
OnLostTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
// 공격 범위 확인
|
||||
if (distanceToPlayer <= attackRange)
|
||||
{
|
||||
TransitionToState(EnemyAIState.Attack);
|
||||
return;
|
||||
}
|
||||
|
||||
// 플레이어 추적 - 매 프레임 업데이트
|
||||
if (_agent.isOnNavMesh && !_agent.isStopped)
|
||||
{
|
||||
_agent.SetDestination(targetPlayer.transform.position);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAttack()
|
||||
{
|
||||
// 코어 공격 중인지 확인
|
||||
bool attackingCore = _coreTransform != null &&
|
||||
GetDistanceToCoreSurface() <= attackRange * 1.2f;
|
||||
|
||||
if (attackingCore)
|
||||
{
|
||||
float distanceToCore = GetDistanceToCoreSurface();
|
||||
|
||||
if (distanceToCore > attackRange * 1.2f)
|
||||
{
|
||||
TransitionToState(EnemyAIState.MoveToCore);
|
||||
return;
|
||||
}
|
||||
|
||||
// 코어를 바라보기
|
||||
Vector3 directionToCore = (_coreTransform.position - transform.position).normalized;
|
||||
directionToCore.y = 0;
|
||||
if (directionToCore != Vector3.zero)
|
||||
{
|
||||
Quaternion targetRotation = Quaternion.LookRotation(directionToCore);
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * 5f);
|
||||
}
|
||||
|
||||
// 코어 공격
|
||||
IDamageable coreHealth = _coreTransform.GetComponent<IDamageable>();
|
||||
if (coreHealth != null && Time.time - _lastAttackTime >= attackInterval)
|
||||
{
|
||||
coreHealth.TakeDamage(attackDamage, NetworkObjectId);
|
||||
_lastAttackTime = Time.time;
|
||||
Debug.Log($"<color=red>[EnemyAI] {gameObject.name}이(가) 코어를 공격! (데미지: {attackDamage}, 표면 거리: {distanceToCore:F2}m)</color>");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 플레이어 공격
|
||||
GameObject targetPlayer = GetTargetPlayer();
|
||||
|
||||
if (targetPlayer == null)
|
||||
{
|
||||
OnLostTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
float distanceToPlayer = Vector3.Distance(transform.position, targetPlayer.transform.position);
|
||||
|
||||
if (distanceToPlayer > attackRange * 1.2f)
|
||||
{
|
||||
TransitionToState(EnemyAIState.ChasePlayer);
|
||||
return;
|
||||
}
|
||||
|
||||
// 플레이어를 바라보기
|
||||
Vector3 directionToPlayer = (targetPlayer.transform.position - transform.position).normalized;
|
||||
directionToPlayer.y = 0;
|
||||
if (directionToPlayer != Vector3.zero)
|
||||
{
|
||||
Quaternion targetRotation = Quaternion.LookRotation(directionToPlayer);
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * 5f);
|
||||
}
|
||||
|
||||
// 공격
|
||||
if (Time.time - _lastAttackTime >= attackInterval)
|
||||
{
|
||||
AttackPlayer(targetPlayer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateReturnToOrigin()
|
||||
{
|
||||
GameObject player = DetectPlayer();
|
||||
if (player != null)
|
||||
{
|
||||
SetTargetPlayer(player);
|
||||
TransitionToState(EnemyAIState.ChasePlayer);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_agent.pathPending && _agent.remainingDistance <= _agent.stoppingDistance)
|
||||
{
|
||||
if (!_agent.hasPath || _agent.velocity.sqrMagnitude == 0f)
|
||||
{
|
||||
if (showDebugInfo)
|
||||
{
|
||||
Debug.Log($"<color=green>[EnemyAI] {gameObject.name}이(가) 원래 위치로 복귀했습니다.</color>");
|
||||
}
|
||||
TransitionToState(EnemyAIState.Idle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Detection
|
||||
|
||||
private GameObject DetectPlayer()
|
||||
{
|
||||
Collider[] colliders = Physics.OverlapSphere(transform.position, detectionRange, playerLayer);
|
||||
|
||||
GameObject closestPlayer = null;
|
||||
float closestDistance = float.MaxValue;
|
||||
|
||||
foreach (Collider col in colliders)
|
||||
{
|
||||
// 자기 자신 제외
|
||||
if (col.transform.root == transform.root)
|
||||
continue;
|
||||
|
||||
// 플레이어 팀 확인 (부모에서 찾기)
|
||||
ITeamMember teamMember = col.GetComponentInParent<ITeamMember>();
|
||||
if (teamMember == null || teamMember.GetTeam() != TeamType.Player)
|
||||
continue;
|
||||
|
||||
// 플레이어 위치 (루트 오브젝트 사용)
|
||||
Transform playerRoot = col.transform.root;
|
||||
Vector3 playerPosition = playerRoot.position;
|
||||
|
||||
// 거리 체크
|
||||
float distance = Vector3.Distance(transform.position, playerPosition);
|
||||
if (distance > detectionRange)
|
||||
continue;
|
||||
|
||||
// 시야각 확인 (360도면 모든 방향 감지)
|
||||
if (detectionAngle < 360f)
|
||||
{
|
||||
Vector3 directionToTarget = (playerPosition - transform.position).normalized;
|
||||
float angleToTarget = Vector3.Angle(transform.forward, directionToTarget);
|
||||
|
||||
if (angleToTarget > detectionAngle / 2f)
|
||||
continue;
|
||||
}
|
||||
|
||||
// 시야 체크 (레이캐스트) - 플레이어 중심으로
|
||||
Vector3 rayStart = transform.position + Vector3.up * 1f; // 적의 눈 높이
|
||||
Vector3 rayTarget = playerPosition + Vector3.up * 1f; // 플레이어 중심
|
||||
Vector3 rayDirection = (rayTarget - rayStart).normalized;
|
||||
float rayDistance = Vector3.Distance(rayStart, rayTarget);
|
||||
|
||||
bool lineOfSight = true;
|
||||
|
||||
// 장애물 체크 (옵션)
|
||||
if (Physics.Raycast(rayStart, rayDirection, out RaycastHit hit, rayDistance, obstacleLayer))
|
||||
{
|
||||
// 맞은 오브젝트가 플레이어의 루트나 자식인지 확인
|
||||
if (hit.transform.root != playerRoot)
|
||||
{
|
||||
lineOfSight = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (lineOfSight)
|
||||
{
|
||||
// 가장 가까운 플레이어 찾기
|
||||
if (distance < closestDistance)
|
||||
{
|
||||
closestDistance = distance;
|
||||
closestPlayer = playerRoot.gameObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 감지 성공 시 로그 (1초에 한 번만)
|
||||
if (closestPlayer != null && showDebugInfo && Time.time - _lastDetectionLogTime >= 1f)
|
||||
{
|
||||
string angleInfo = detectionAngle >= 360f ? "전방향" : $"{Vector3.Angle(transform.forward, (closestPlayer.transform.position - transform.position).normalized):F1}°";
|
||||
Debug.Log($"<color=cyan>[EnemyAI] {gameObject.name}이(가) {closestPlayer.name}을(를) 감지! (거리: {closestDistance:F2}m, 각도: {angleInfo})</color>");
|
||||
_lastDetectionLogTime = Time.time;
|
||||
}
|
||||
|
||||
return closestPlayer;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Combat
|
||||
|
||||
private void AttackPlayer(GameObject player)
|
||||
{
|
||||
IDamageable damageable = player.GetComponentInParent<IDamageable>();
|
||||
if (damageable != null)
|
||||
{
|
||||
damageable.TakeDamage(attackDamage, NetworkObjectId);
|
||||
_lastAttackTime = Time.time;
|
||||
|
||||
if (showDebugInfo)
|
||||
{
|
||||
Debug.Log($"<color=red>[EnemyAI] {gameObject.name}이(가) {player.name}을(를) 공격! (데미지: {attackDamage})</color>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Distance Calculation
|
||||
|
||||
private float GetDistanceToCoreSurface()
|
||||
{
|
||||
if (_coreTransform == null)
|
||||
return float.MaxValue;
|
||||
|
||||
if (_coreCollider != null)
|
||||
{
|
||||
Vector3 closestPoint = _coreCollider.ClosestPoint(transform.position);
|
||||
float distanceToSurface = Vector3.Distance(transform.position, closestPoint);
|
||||
return distanceToSurface;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Vector3.Distance(transform.position, _coreTransform.position);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State Management
|
||||
|
||||
private void TransitionToState(EnemyAIState newState)
|
||||
{
|
||||
if (_currentState.Value == newState) return;
|
||||
|
||||
if (showDebugInfo)
|
||||
{
|
||||
Debug.Log($"<color=magenta>[EnemyAI] {gameObject.name} 상태 변경: {_currentState.Value} → {newState}</color>");
|
||||
}
|
||||
|
||||
OnExitState(_currentState.Value);
|
||||
_currentState.Value = newState;
|
||||
OnEnterState(newState);
|
||||
}
|
||||
|
||||
private void OnEnterState(EnemyAIState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case EnemyAIState.Idle:
|
||||
_agent.isStopped = true;
|
||||
_agent.speed = moveSpeed;
|
||||
_agent.ResetPath();
|
||||
break;
|
||||
|
||||
case EnemyAIState.MoveToCore:
|
||||
_agent.isStopped = false;
|
||||
_agent.speed = moveSpeed;
|
||||
_hasSetCoreDestination = false;
|
||||
|
||||
if (showDebugInfo)
|
||||
{
|
||||
Debug.Log($"<color=cyan>[EnemyAI] {gameObject.name}이(가) 코어로 이동 시작</color>");
|
||||
}
|
||||
break;
|
||||
|
||||
case EnemyAIState.ChasePlayer:
|
||||
_agent.isStopped = false;
|
||||
_agent.speed = moveSpeed * chaseSpeedMultiplier;
|
||||
_chaseStartPosition = transform.position;
|
||||
|
||||
if (showDebugInfo)
|
||||
{
|
||||
Debug.Log($"<color=cyan>[EnemyAI] {gameObject.name}이(가) 추적 시작! (시작 위치: {_chaseStartPosition})</color>");
|
||||
}
|
||||
break;
|
||||
|
||||
case EnemyAIState.Attack:
|
||||
_agent.isStopped = true;
|
||||
_agent.ResetPath();
|
||||
break;
|
||||
|
||||
case EnemyAIState.ReturnToOrigin:
|
||||
_agent.isStopped = false;
|
||||
_agent.speed = moveSpeed;
|
||||
_agent.stoppingDistance = 1f;
|
||||
_agent.SetDestination(_originPosition);
|
||||
ClearTargetPlayer();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnExitState(EnemyAIState state)
|
||||
{
|
||||
if (state == EnemyAIState.ReturnToOrigin)
|
||||
{
|
||||
_agent.stoppingDistance = attackRange * 0.7f;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLostTarget()
|
||||
{
|
||||
if (aiType == TeamType.Hostile)
|
||||
{
|
||||
TransitionToState(EnemyAIState.ReturnToOrigin);
|
||||
}
|
||||
else if (aiType == TeamType.Monster)
|
||||
{
|
||||
ClearTargetPlayer();
|
||||
_hasSetCoreDestination = false;
|
||||
TransitionToState(EnemyAIState.MoveToCore);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Target Management
|
||||
|
||||
private void SetTargetPlayer(GameObject player)
|
||||
{
|
||||
var networkObject = player.GetComponentInParent<NetworkObject>();
|
||||
if (networkObject != null)
|
||||
{
|
||||
_targetPlayerId.Value = networkObject.NetworkObjectId;
|
||||
_cachedTargetPlayer = player;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearTargetPlayer()
|
||||
{
|
||||
_targetPlayerId.Value = 0;
|
||||
_cachedTargetPlayer = null;
|
||||
}
|
||||
|
||||
private GameObject GetTargetPlayer()
|
||||
{
|
||||
if (_targetPlayerId.Value == 0) return null;
|
||||
|
||||
if (_cachedTargetPlayer != null && _cachedTargetPlayer.activeSelf)
|
||||
{
|
||||
return _cachedTargetPlayer;
|
||||
}
|
||||
|
||||
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(_targetPlayerId.Value, out NetworkObject networkObject))
|
||||
{
|
||||
_cachedTargetPlayer = networkObject.gameObject;
|
||||
return _cachedTargetPlayer;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Utilities
|
||||
|
||||
private void FindCore()
|
||||
{
|
||||
Core core = FindFirstObjectByType<Core>();
|
||||
if (core != null)
|
||||
{
|
||||
_coreTransform = core.transform;
|
||||
|
||||
_coreCollider = core.GetComponent<Collider>();
|
||||
if (_coreCollider == null)
|
||||
{
|
||||
_coreCollider = core.GetComponentInChildren<Collider>();
|
||||
}
|
||||
|
||||
if (_coreCollider != null)
|
||||
{
|
||||
Debug.Log($"<color=green>[EnemyAI] {gameObject.name}이(가) 코어를 찾았습니다! (위치: {_coreTransform.position}, Collider: {_coreCollider.GetType().Name})</color>");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"<color=orange>[EnemyAI] {gameObject.name}이(가) 코어를 찾았지만 Collider가 없습니다.</color>");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"<color=orange>[EnemyAI] {gameObject.name}이(가) 코어를 찾을 수 없습니다!</color>");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Gizmos
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
if (!showDebugInfo) return;
|
||||
|
||||
// 감지 범위
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawWireSphere(transform.position, detectionRange);
|
||||
|
||||
// 공격 범위
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(transform.position, attackRange);
|
||||
|
||||
// 시야각 (360도가 아닐 때만 표시)
|
||||
if (detectionAngle < 360f)
|
||||
{
|
||||
Vector3 forward = transform.forward * detectionRange;
|
||||
Vector3 leftBoundary = Quaternion.Euler(0, -detectionAngle / 2f, 0) * forward;
|
||||
Vector3 rightBoundary = Quaternion.Euler(0, detectionAngle / 2f, 0) * forward;
|
||||
|
||||
Gizmos.color = Color.blue;
|
||||
Gizmos.DrawLine(transform.position, transform.position + leftBoundary);
|
||||
Gizmos.DrawLine(transform.position, transform.position + rightBoundary);
|
||||
}
|
||||
|
||||
// 원점 표시 (적대 세력만)
|
||||
if (aiType == TeamType.Hostile && Application.isPlaying)
|
||||
{
|
||||
Gizmos.color = Color.green;
|
||||
Gizmos.DrawWireSphere(_originPosition, 1f);
|
||||
Gizmos.DrawLine(transform.position, _originPosition);
|
||||
}
|
||||
|
||||
// 추적 시작 위치 표시
|
||||
if (Application.isPlaying && (_currentState.Value == EnemyAIState.ChasePlayer || _currentState.Value == EnemyAIState.Attack))
|
||||
{
|
||||
Gizmos.color = Color.cyan;
|
||||
Gizmos.DrawWireSphere(_chaseStartPosition, 1.5f);
|
||||
Gizmos.DrawLine(transform.position, _chaseStartPosition);
|
||||
}
|
||||
|
||||
// 코어 방향 및 표면까지의 거리 표시
|
||||
if (Application.isPlaying && _currentState.Value == EnemyAIState.MoveToCore && _coreTransform != null)
|
||||
{
|
||||
Gizmos.color = Color.magenta;
|
||||
Gizmos.DrawLine(transform.position, _coreTransform.position);
|
||||
Gizmos.DrawWireSphere(_coreTransform.position, 2f);
|
||||
|
||||
if (_coreCollider != null)
|
||||
{
|
||||
Vector3 closestPoint = _coreCollider.ClosestPoint(transform.position);
|
||||
Gizmos.color = Color.green;
|
||||
Gizmos.DrawLine(transform.position, closestPoint);
|
||||
Gizmos.DrawWireSphere(closestPoint, 0.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
OnDrawGizmos();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (Application.isPlaying && _agent != null)
|
||||
{
|
||||
string pathInfo = _agent.hasPath ? $"Path: {_agent.path.status}" : "No Path";
|
||||
string navMeshInfo = _agent.isOnNavMesh ? "On NavMesh" : "OFF NAVMESH!";
|
||||
string velocityInfo = $"Velocity: {_agent.velocity.magnitude:F2}";
|
||||
|
||||
string distanceInfo = "";
|
||||
if (_coreTransform != null && _currentState.Value == EnemyAIState.MoveToCore)
|
||||
{
|
||||
float surfaceDistance = GetDistanceToCoreSurface();
|
||||
distanceInfo = $"\nCore Surface Dist: {surfaceDistance:F2}m";
|
||||
}
|
||||
|
||||
string angleInfo = detectionAngle >= 360f ? "\nDetection: 360° (전방향)" : $"\nDetection: {detectionAngle}°";
|
||||
|
||||
UnityEditor.Handles.Label(transform.position + Vector3.up * 3f,
|
||||
$"Enemy AI\nState: {_currentState.Value}\nType: {aiType}\n{navMeshInfo}\n{pathInfo}\n{velocityInfo}{angleInfo}\nRange: {detectionRange}m\nAttack: {attackRange}m{distanceInfo}");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/EnemyAIController.cs.meta
Normal file
2
Assets/Scripts/EnemyAIController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 453e726e48d16214f84c6d5737edd7df
|
||||
14
Assets/Scripts/EnemyAIState.cs
Normal file
14
Assets/Scripts/EnemyAIState.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Northbound
|
||||
{
|
||||
/// <summary>
|
||||
/// 적 AI의 상태
|
||||
/// </summary>
|
||||
public enum EnemyAIState
|
||||
{
|
||||
Idle, // 대기 (적대 세력 기본 상태)
|
||||
MoveToCore, // 코어로 이동 (몬스터 기본 상태)
|
||||
ChasePlayer, // 플레이어 추적
|
||||
Attack, // 공격
|
||||
ReturnToOrigin // 원래 위치로 복귀 (적대 세력)
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/EnemyAIState.cs.meta
Normal file
2
Assets/Scripts/EnemyAIState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f34add9794647d043ae26b47fac8e429
|
||||
191
Assets/Scripts/EnemyUnit.cs
Normal file
191
Assets/Scripts/EnemyUnit.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound
|
||||
{
|
||||
/// <summary>
|
||||
/// 적대 유닛 (적대세력 또는 몬스터)
|
||||
/// </summary>
|
||||
public class EnemyUnit : NetworkBehaviour, IDamageable, ITeamMember, IVisionProvider
|
||||
{
|
||||
[Header("Team Settings")]
|
||||
[Tooltip("이 유닛의 팀 (Hostile = 적대세력, Monster = 몬스터)")]
|
||||
public TeamType enemyTeam = TeamType.Hostile;
|
||||
|
||||
[Header("Combat")]
|
||||
public int maxHealth = 100;
|
||||
public float visionRange = 10f;
|
||||
|
||||
[Header("Visual")]
|
||||
public GameObject damageEffectPrefab;
|
||||
public GameObject destroyEffectPrefab;
|
||||
|
||||
private NetworkVariable<int> _currentHealth = new NetworkVariable<int>(
|
||||
0,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
private NetworkVariable<TeamType> _team = new NetworkVariable<TeamType>(
|
||||
TeamType.Neutral,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
base.OnNetworkSpawn();
|
||||
|
||||
if (IsServer)
|
||||
{
|
||||
_currentHealth.Value = maxHealth;
|
||||
_team.Value = enemyTeam;
|
||||
|
||||
// FogOfWar 시스템에 등록
|
||||
FogOfWarSystem.Instance?.RegisterVisionProvider(this);
|
||||
|
||||
Debug.Log($"<color=magenta>[EnemyUnit] {gameObject.name} 스폰됨 (팀: {TeamManager.GetTeamName(_team.Value)})</color>");
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
base.OnNetworkDespawn();
|
||||
|
||||
if (IsServer)
|
||||
{
|
||||
FogOfWarSystem.Instance?.UnregisterVisionProvider(this);
|
||||
}
|
||||
}
|
||||
|
||||
#region IDamageable Implementation
|
||||
|
||||
public void TakeDamage(int damage, ulong attackerId)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
if (_currentHealth.Value <= 0) return;
|
||||
|
||||
// 공격자의 팀 확인
|
||||
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(attackerId, out NetworkObject attackerObj))
|
||||
{
|
||||
var attackerTeamMember = attackerObj.GetComponent<ITeamMember>();
|
||||
if (attackerTeamMember != null)
|
||||
{
|
||||
if (!TeamManager.CanAttack(attackerTeamMember, this))
|
||||
{
|
||||
Debug.Log($"<color=yellow>[EnemyUnit] {TeamManager.GetTeamName(attackerTeamMember.GetTeam())} 팀은 {TeamManager.GetTeamName(_team.Value)} 팀을 공격할 수 없습니다.</color>");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int actualDamage = Mathf.Min(damage, _currentHealth.Value);
|
||||
_currentHealth.Value -= actualDamage;
|
||||
|
||||
Debug.Log($"<color=red>[EnemyUnit] {gameObject.name} ({TeamManager.GetTeamName(_team.Value)})이(가) {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{maxHealth}</color>");
|
||||
|
||||
// 데미지 이펙트
|
||||
ShowDamageEffectClientRpc();
|
||||
|
||||
// 체력이 0이 되면 파괴
|
||||
if (_currentHealth.Value <= 0)
|
||||
{
|
||||
DestroyUnit(attackerId);
|
||||
}
|
||||
}
|
||||
|
||||
private void DestroyUnit(ulong attackerId)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
Debug.Log($"<color=red>[EnemyUnit] {gameObject.name} ({TeamManager.GetTeamName(_team.Value)})이(가) 파괴되었습니다! (공격자: {attackerId})</color>");
|
||||
|
||||
// 파괴 이펙트
|
||||
ShowDestroyEffectClientRpc();
|
||||
|
||||
// FogOfWar 시스템에서 제거
|
||||
FogOfWarSystem.Instance?.UnregisterVisionProvider(this);
|
||||
|
||||
// 네트워크 오브젝트 파괴
|
||||
Invoke(nameof(DespawnUnit), 0.5f);
|
||||
}
|
||||
|
||||
private void DespawnUnit()
|
||||
{
|
||||
if (IsServer && NetworkObject != null)
|
||||
{
|
||||
NetworkObject.Despawn(true);
|
||||
}
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
private void ShowDamageEffectClientRpc()
|
||||
{
|
||||
if (damageEffectPrefab != null)
|
||||
{
|
||||
GameObject effect = Instantiate(damageEffectPrefab, transform.position, Quaternion.identity);
|
||||
Destroy(effect, 2f);
|
||||
}
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
private void ShowDestroyEffectClientRpc()
|
||||
{
|
||||
if (destroyEffectPrefab != null)
|
||||
{
|
||||
GameObject effect = Instantiate(destroyEffectPrefab, transform.position, Quaternion.identity);
|
||||
Destroy(effect, 3f);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ITeamMember Implementation
|
||||
|
||||
public TeamType GetTeam() => _team.Value;
|
||||
|
||||
public void SetTeam(TeamType team)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
_team.Value = team;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IVisionProvider Implementation
|
||||
|
||||
public ulong GetOwnerId() => OwnerClientId;
|
||||
|
||||
public float GetVisionRange() => visionRange;
|
||||
|
||||
public Transform GetTransform() => transform;
|
||||
|
||||
public bool IsActive() => IsSpawned && _currentHealth.Value > 0;
|
||||
|
||||
#endregion
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
// 팀 색상으로 시야 범위 표시
|
||||
Color teamColor = Application.isPlaying
|
||||
? TeamManager.GetTeamColor(_team.Value)
|
||||
: TeamManager.GetTeamColor(enemyTeam);
|
||||
|
||||
Gizmos.color = new Color(teamColor.r, teamColor.g, teamColor.b, 0.3f);
|
||||
Gizmos.DrawWireSphere(transform.position, visionRange);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
UnityEditor.Handles.Label(transform.position + Vector3.up * 2f,
|
||||
$"Team: {TeamManager.GetTeamName(_team.Value)}\nHP: {_currentHealth.Value}/{maxHealth}");
|
||||
}
|
||||
else
|
||||
{
|
||||
UnityEditor.Handles.Label(transform.position + Vector3.up * 2f,
|
||||
$"Team: {TeamManager.GetTeamName(enemyTeam)}");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/EnemyUnit.cs.meta
Normal file
2
Assets/Scripts/EnemyUnit.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 345fc6e7d4f06314f8b548129700eccb
|
||||
11
Assets/Scripts/ITeamMember.cs
Normal file
11
Assets/Scripts/ITeamMember.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Northbound
|
||||
{
|
||||
/// <summary>
|
||||
/// 팀에 속한 엔티티
|
||||
/// </summary>
|
||||
public interface ITeamMember
|
||||
{
|
||||
TeamType GetTeam();
|
||||
void SetTeam(TeamType team);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/ITeamMember.cs.meta
Normal file
2
Assets/Scripts/ITeamMember.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e343729a9b720e438ce09faa7886ab0
|
||||
@@ -2,13 +2,37 @@ using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using Unity.Cinemachine;
|
||||
using Northbound;
|
||||
|
||||
public class NetworkPlayerController : NetworkBehaviour
|
||||
public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageable
|
||||
{
|
||||
[Header("Movement Settings")]
|
||||
public float moveSpeed = 5f;
|
||||
public float rotationSpeed = 10f;
|
||||
|
||||
[Header("Team Settings")]
|
||||
[SerializeField] private TeamType initialTeam = TeamType.Player;
|
||||
|
||||
[Header("Health Settings")]
|
||||
[SerializeField] private int maxHealth = 100;
|
||||
[SerializeField] private bool showHealthBar = true;
|
||||
|
||||
[Header("Visual Effects")]
|
||||
[SerializeField] private GameObject damageEffectPrefab;
|
||||
[SerializeField] private GameObject deathEffectPrefab;
|
||||
|
||||
private NetworkVariable<TeamType> _team = new NetworkVariable<TeamType>(
|
||||
TeamType.Player,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
private NetworkVariable<int> _currentHealth = new NetworkVariable<int>(
|
||||
100,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
private Vector2 _moveInput;
|
||||
private CharacterController _controller;
|
||||
private PlayerInputActions _inputActions;
|
||||
@@ -22,6 +46,27 @@ public class NetworkPlayerController : NetworkBehaviour
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
base.OnNetworkSpawn();
|
||||
|
||||
// 서버에서 초기화
|
||||
if (IsServer)
|
||||
{
|
||||
if (_team.Value == TeamType.Neutral)
|
||||
{
|
||||
_team.Value = initialTeam;
|
||||
}
|
||||
|
||||
if (_currentHealth.Value == 0)
|
||||
{
|
||||
_currentHealth.Value = maxHealth;
|
||||
}
|
||||
|
||||
Debug.Log($"<color=cyan>[Player] {gameObject.name} 스폰됨 (팀: {TeamManager.GetTeamName(_team.Value)}, 체력: {_currentHealth.Value}/{maxHealth})</color>");
|
||||
}
|
||||
|
||||
// 체력 변경 이벤트 구독
|
||||
_currentHealth.OnValueChanged += OnHealthChanged;
|
||||
|
||||
if (!IsOwner) return;
|
||||
|
||||
var vcam = GameObject.FindFirstObjectByType<CinemachineCamera>();
|
||||
@@ -39,16 +84,23 @@ public class NetworkPlayerController : NetworkBehaviour
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
_currentHealth.OnValueChanged -= OnHealthChanged;
|
||||
|
||||
if (IsOwner && _inputActions != null)
|
||||
{
|
||||
_inputActions.Disable();
|
||||
}
|
||||
|
||||
base.OnNetworkDespawn();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!IsOwner) return;
|
||||
|
||||
// 죽었으면 이동 불가
|
||||
if (_currentHealth.Value <= 0) return;
|
||||
|
||||
_moveInput = _inputActions.Player.Move.ReadValue<Vector2>();
|
||||
Vector3 move = new Vector3(_moveInput.x, 0, _moveInput.y).normalized;
|
||||
|
||||
@@ -68,4 +120,201 @@ public class NetworkPlayerController : NetworkBehaviour
|
||||
_animator.SetFloat("MoveSpeed", move.magnitude);
|
||||
}
|
||||
}
|
||||
|
||||
#region ITeamMember Implementation
|
||||
|
||||
public TeamType GetTeam() => _team.Value;
|
||||
|
||||
public void SetTeam(TeamType team)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
TeamType previousTeam = _team.Value;
|
||||
_team.Value = team;
|
||||
Debug.Log($"<color=cyan>[Player] 팀 변경: {TeamManager.GetTeamName(previousTeam)} → {TeamManager.GetTeamName(team)}</color>");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDamageable Implementation
|
||||
|
||||
public void TakeDamage(int damage, ulong attackerId)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
// 이미 죽었으면 무시
|
||||
if (_currentHealth.Value <= 0) return;
|
||||
|
||||
// 공격자의 팀 확인
|
||||
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(attackerId, out NetworkObject attackerObj))
|
||||
{
|
||||
var attackerTeamMember = attackerObj.GetComponent<ITeamMember>();
|
||||
if (attackerTeamMember != null)
|
||||
{
|
||||
if (!TeamManager.CanAttack(attackerTeamMember, this))
|
||||
{
|
||||
Debug.Log($"<color=yellow>[Player] {TeamManager.GetTeamName(attackerTeamMember.GetTeam())} 팀은 {TeamManager.GetTeamName(_team.Value)} 팀을 공격할 수 없습니다.</color>");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 데미지 적용
|
||||
int actualDamage = Mathf.Min(damage, _currentHealth.Value);
|
||||
_currentHealth.Value -= actualDamage;
|
||||
|
||||
Debug.Log($"<color=red>[Player] {gameObject.name} ({TeamManager.GetTeamName(_team.Value)})이(가) {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{maxHealth}</color>");
|
||||
|
||||
// 데미지 이펙트
|
||||
ShowDamageEffectClientRpc();
|
||||
|
||||
// 체력이 0이 되면 사망
|
||||
if (_currentHealth.Value <= 0)
|
||||
{
|
||||
Die(attackerId);
|
||||
}
|
||||
}
|
||||
|
||||
private void Die(ulong killerId)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
Debug.Log($"<color=red>[Player] {gameObject.name} ({TeamManager.GetTeamName(_team.Value)})이(가) 사망했습니다! (킬러: {killerId})</color>");
|
||||
|
||||
// 사망 이펙트
|
||||
ShowDeathEffectClientRpc();
|
||||
|
||||
// 애니메이션 (있는 경우)
|
||||
if (_animator != null)
|
||||
{
|
||||
_animator.SetTrigger("Die");
|
||||
}
|
||||
|
||||
// 일정 시간 후 리스폰 또는 디스폰
|
||||
Invoke(nameof(HandleDeath), 3f);
|
||||
}
|
||||
|
||||
private void HandleDeath()
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
// 여기서 리스폰 로직을 추가하거나 게임 오버 처리
|
||||
// 예: 리스폰 위치로 이동 및 체력 회복
|
||||
Respawn();
|
||||
}
|
||||
|
||||
private void Respawn()
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
// 체력 회복
|
||||
_currentHealth.Value = maxHealth;
|
||||
|
||||
// 스폰 포인트로 이동 (PlayerSpawnPoint 활용)
|
||||
var spawnPoints = FindObjectsByType<PlayerSpawnPoint>(FindObjectsSortMode.None);
|
||||
if (spawnPoints.Length > 0)
|
||||
{
|
||||
var spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)];
|
||||
transform.position = spawnPoint.transform.position;
|
||||
transform.rotation = spawnPoint.transform.rotation;
|
||||
}
|
||||
|
||||
Debug.Log($"<color=green>[Player] {gameObject.name} 리스폰!</color>");
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
private void ShowDamageEffectClientRpc()
|
||||
{
|
||||
if (damageEffectPrefab != null)
|
||||
{
|
||||
GameObject effect = Instantiate(damageEffectPrefab, transform.position + Vector3.up, Quaternion.identity);
|
||||
Destroy(effect, 2f);
|
||||
}
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
private void ShowDeathEffectClientRpc()
|
||||
{
|
||||
if (deathEffectPrefab != null)
|
||||
{
|
||||
GameObject effect = Instantiate(deathEffectPrefab, transform.position, Quaternion.identity);
|
||||
Destroy(effect, 3f);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Health Management
|
||||
|
||||
/// <summary>
|
||||
/// 현재 체력
|
||||
/// </summary>
|
||||
public int GetCurrentHealth() => _currentHealth.Value;
|
||||
|
||||
/// <summary>
|
||||
/// 최대 체력
|
||||
/// </summary>
|
||||
public int GetMaxHealth() => maxHealth;
|
||||
|
||||
/// <summary>
|
||||
/// 체력 비율 (0.0 ~ 1.0)
|
||||
/// </summary>
|
||||
public float GetHealthPercentage()
|
||||
{
|
||||
return maxHealth > 0 ? (float)_currentHealth.Value / maxHealth : 0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 죽었는지 여부
|
||||
/// </summary>
|
||||
public bool IsDead() => _currentHealth.Value <= 0;
|
||||
|
||||
/// <summary>
|
||||
/// 체력 회복
|
||||
/// </summary>
|
||||
public void Heal(int amount)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
int healAmount = Mathf.Min(amount, maxHealth - _currentHealth.Value);
|
||||
_currentHealth.Value += healAmount;
|
||||
|
||||
Debug.Log($"<color=green>[Player] {gameObject.name}이(가) {healAmount} 회복되었습니다. 현재 체력: {_currentHealth.Value}/{maxHealth}</color>");
|
||||
}
|
||||
|
||||
private void OnHealthChanged(int previousValue, int newValue)
|
||||
{
|
||||
// 체력바 UI 업데이트 또는 체력 변경 시각 효과
|
||||
Debug.Log($"<color=yellow>[Player] 체력 변경: {previousValue} → {newValue}</color>");
|
||||
|
||||
// 클라이언트에서도 체력 변경 인지 가능
|
||||
if (IsOwner)
|
||||
{
|
||||
// UI 업데이트 등
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Gizmos
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
string teamName = TeamManager.GetTeamName(_team.Value);
|
||||
UnityEditor.Handles.Label(transform.position + Vector3.up * 3f,
|
||||
$"Player: {gameObject.name}\nTeam: {teamName}\nHP: {_currentHealth.Value}/{maxHealth}");
|
||||
}
|
||||
else
|
||||
{
|
||||
string teamName = TeamManager.GetTeamName(initialTeam);
|
||||
UnityEditor.Handles.Label(transform.position + Vector3.up * 3f,
|
||||
$"Player: {gameObject.name}\nTeam: {teamName}\nHP: {maxHealth}/{maxHealth}");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
96
Assets/Scripts/TeamManager.cs
Normal file
96
Assets/Scripts/TeamManager.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound
|
||||
{
|
||||
/// <summary>
|
||||
/// 팀 간의 관계 및 적대 관계를 관리
|
||||
/// </summary>
|
||||
public static class TeamManager
|
||||
{
|
||||
// 팀 간 적대 관계 테이블
|
||||
private static readonly Dictionary<(TeamType, TeamType), bool> _hostilityTable = new Dictionary<(TeamType, TeamType), bool>
|
||||
{
|
||||
// 플레이어 vs 적대 세력
|
||||
{ (TeamType.Player, TeamType.Hostile), true },
|
||||
{ (TeamType.Hostile, TeamType.Player), true },
|
||||
|
||||
// 플레이어 vs 몬스터
|
||||
{ (TeamType.Player, TeamType.Monster), true },
|
||||
{ (TeamType.Monster, TeamType.Player), true },
|
||||
|
||||
// 적대 세력 vs 몬스터 (서로 공격하지 않음)
|
||||
{ (TeamType.Hostile, TeamType.Monster), false },
|
||||
{ (TeamType.Monster, TeamType.Hostile), false },
|
||||
|
||||
// 같은 팀끼리는 공격하지 않음
|
||||
{ (TeamType.Player, TeamType.Player), false },
|
||||
{ (TeamType.Hostile, TeamType.Hostile), false },
|
||||
{ (TeamType.Monster, TeamType.Monster), false },
|
||||
|
||||
// 중립은 공격받지 않음
|
||||
{ (TeamType.Neutral, TeamType.Player), false },
|
||||
{ (TeamType.Neutral, TeamType.Hostile), false },
|
||||
{ (TeamType.Neutral, TeamType.Monster), false },
|
||||
{ (TeamType.Player, TeamType.Neutral), false },
|
||||
{ (TeamType.Hostile, TeamType.Neutral), false },
|
||||
{ (TeamType.Monster, TeamType.Neutral), false },
|
||||
{ (TeamType.Neutral, TeamType.Neutral), false }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 두 팀이 적대 관계인지 확인
|
||||
/// </summary>
|
||||
public static bool AreHostile(TeamType team1, TeamType team2)
|
||||
{
|
||||
if (_hostilityTable.TryGetValue((team1, team2), out bool isHostile))
|
||||
{
|
||||
return isHostile;
|
||||
}
|
||||
|
||||
// 기본적으로 다른 팀이면 적대
|
||||
return team1 != team2 && team1 != TeamType.Neutral && team2 != TeamType.Neutral;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 공격 가능한 대상인지 확인
|
||||
/// </summary>
|
||||
public static bool CanAttack(ITeamMember attacker, ITeamMember target)
|
||||
{
|
||||
if (attacker == null || target == null)
|
||||
return false;
|
||||
|
||||
return AreHostile(attacker.GetTeam(), target.GetTeam());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 팀의 색상 가져오기 (UI 표시용)
|
||||
/// </summary>
|
||||
public static Color GetTeamColor(TeamType team)
|
||||
{
|
||||
return team switch
|
||||
{
|
||||
TeamType.Player => Color.blue,
|
||||
TeamType.Hostile => Color.red,
|
||||
TeamType.Monster => new Color(0.8f, 0f, 0.8f), // 보라색
|
||||
TeamType.Neutral => Color.gray,
|
||||
_ => Color.white
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 팀 이름 가져오기 (한글)
|
||||
/// </summary>
|
||||
public static string GetTeamName(TeamType team)
|
||||
{
|
||||
return team switch
|
||||
{
|
||||
TeamType.Player => "플레이어",
|
||||
TeamType.Hostile => "적대 세력",
|
||||
TeamType.Monster => "몬스터",
|
||||
TeamType.Neutral => "중립",
|
||||
_ => "알 수 없음"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/TeamManager.cs.meta
Normal file
2
Assets/Scripts/TeamManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95252f6c80f5e2a40b0d4f95c23b2039
|
||||
13
Assets/Scripts/TeamType.cs
Normal file
13
Assets/Scripts/TeamType.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Northbound
|
||||
{
|
||||
/// <summary>
|
||||
/// 게임 내 팀 타입
|
||||
/// </summary>
|
||||
public enum TeamType
|
||||
{
|
||||
Neutral = 0, // 중립 (공격받지 않음)
|
||||
Player = 1, // 플레이어 팀
|
||||
Hostile = 2, // 적대 세력 (플레이어 공격)
|
||||
Monster = 3 // 몬스터 (플레이어 공격)
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/TeamType.cs.meta
Normal file
2
Assets/Scripts/TeamType.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa67976f76e9a5f4fac1e55e7bfedf52
|
||||
Reference in New Issue
Block a user