보스 몬스터 구현중
This commit is contained in:
parent
2a4c20435f
commit
f6025cc692
Binary file not shown.
Binary file not shown.
|
|
@ -4406,6 +4406,10 @@ PrefabInstance:
|
|||
propertyPath: m_Name
|
||||
value: SwordMonster
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5674935864780053661, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3}
|
||||
propertyPath: m_IsActive
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
value: 19.30664
|
||||
|
|
@ -9125,10 +9129,10 @@ MonoBehaviour:
|
|||
m_Script: {fileID: 11500000, guid: 1f525fb5022b0754b9c5e1d725f8b2a4, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
spawnRange: 15
|
||||
respawnCooldown: 3
|
||||
mobTag: Throw Monster
|
||||
optimizationRange: 60
|
||||
monsterPrefabs: []
|
||||
spawnPoints: []
|
||||
spawnInterval: 5
|
||||
maxMonsters: 20
|
||||
--- !u!4 &85056390
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -39081,165 +39085,17 @@ Transform:
|
|||
m_CorrespondingSourceObject: {fileID: 4899585634617078, guid: 9b42f9d14b52bd840afbef0e34f754f2, type: 3}
|
||||
m_PrefabInstance: {fileID: 358590105}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!1 &358757529 stripped
|
||||
GameObject:
|
||||
m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
m_PrefabInstance: {fileID: 1101415473}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!114 &358757530
|
||||
--- !u!114 &358757531 stripped
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_CorrespondingSourceObject: {fileID: 9108673629704904850, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
m_PrefabInstance: {fileID: 7311604695488881352}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 358757529}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5e36870a705f4fd44923bc5704c4a93b, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
config: {fileID: 11400000, guid: 4c6a2993f687f65409c4ece370872444, type: 2}
|
||||
OnCounterActivated:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnCounterDeactivated:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnCounterPatternSelected:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnHabitChangeRewarded:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
bossPatterns:
|
||||
- patternName: Smash
|
||||
baseWeight: 30
|
||||
counterType: 1
|
||||
subCounterType: 0
|
||||
- patternName: DashAttack
|
||||
baseWeight: 30
|
||||
counterType: 2
|
||||
subCounterType: 0
|
||||
- patternName: ShieldWall
|
||||
baseWeight: 20
|
||||
counterType: 3
|
||||
subCounterType: 0
|
||||
--- !u!114 &358757531
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 358757529}
|
||||
m_GameObject: {fileID: 1231090171}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 04f39bf969074e6488f84b47bd517dc9, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
optimizationDistance: 40
|
||||
maxHP: 100
|
||||
attackDamage: 10
|
||||
expReward: 10
|
||||
moveSpeed: 3.5
|
||||
myWeapon: {fileID: 0}
|
||||
Monster_Idle: Monster_Idle
|
||||
Monster_GetDamage: Monster_GetDamage
|
||||
Monster_Die: Monster_Die
|
||||
attackRestDuration: 1.5
|
||||
hitSound: {fileID: 0}
|
||||
deathSound: {fileID: 0}
|
||||
deathEffectPrefab: {fileID: 0}
|
||||
hitEffect: {fileID: 0}
|
||||
impactSpawnPoint: {fileID: 0}
|
||||
counterSystem: {fileID: 358757530}
|
||||
patternInterval: 3
|
||||
attackRange: 3
|
||||
ironBall: {fileID: 1550577165}
|
||||
handHolder: {fileID: 1325714516}
|
||||
bossHealthBar: {fileID: 0}
|
||||
anim_Roar: Roar
|
||||
anim_Idle: Monster_Idle
|
||||
anim_Walk: Monster_Walk
|
||||
anim_Pickup: Skill_Pickup
|
||||
anim_Throw: Attack_Throw
|
||||
anim_DashReady: Skill_Dash_Ready
|
||||
anim_DashGo: Skill_Dash_Go
|
||||
anim_SmashCharge: Skill_Smash_Charge
|
||||
anim_SmashImpact: Skill_Smash_Impact
|
||||
anim_Sweep: Skill_Sweep
|
||||
debugState: "\uB300\uAE30 \uC911"
|
||||
--- !u!136 &358757532
|
||||
CapsuleCollider:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 358757529}
|
||||
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: 2
|
||||
m_Radius: 0.5
|
||||
m_Height: 2.02
|
||||
m_Direction: 1
|
||||
m_Center: {x: 0, y: 0.9, z: 0}
|
||||
--- !u!195 &358757533
|
||||
NavMeshAgent:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 358757529}
|
||||
m_Enabled: 1
|
||||
m_AgentTypeID: 0
|
||||
m_Radius: 0.5
|
||||
m_Speed: 3.5
|
||||
m_Acceleration: 8
|
||||
avoidancePriority: 50
|
||||
m_AngularSpeed: 120
|
||||
m_StoppingDistance: 0
|
||||
m_AutoTraverseOffMeshLink: 1
|
||||
m_AutoBraking: 1
|
||||
m_AutoRepath: 1
|
||||
m_Height: 2
|
||||
m_BaseOffset: 0
|
||||
m_WalkableMask: 4294967295
|
||||
m_ObstacleAvoidanceType: 4
|
||||
--- !u!54 &358757536
|
||||
Rigidbody:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 358757529}
|
||||
serializedVersion: 4
|
||||
m_Mass: 1
|
||||
m_Drag: 0
|
||||
m_AngularDrag: 0.05
|
||||
m_CenterOfMass: {x: 0, y: 0, z: 0}
|
||||
m_InertiaTensor: {x: 1, y: 1, z: 1}
|
||||
m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ImplicitCom: 1
|
||||
m_ImplicitTensor: 1
|
||||
m_UseGravity: 1
|
||||
m_IsKinematic: 1
|
||||
m_Interpolate: 0
|
||||
m_Constraints: 80
|
||||
m_CollisionDetection: 0
|
||||
--- !u!1001 &358842824
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -66111,10 +65967,10 @@ MonoBehaviour:
|
|||
m_Script: {fileID: 11500000, guid: 1f525fb5022b0754b9c5e1d725f8b2a4, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
spawnRange: 15
|
||||
respawnCooldown: 3
|
||||
mobTag: Sword Monster
|
||||
optimizationRange: 60
|
||||
monsterPrefabs: []
|
||||
spawnPoints: []
|
||||
spawnInterval: 5
|
||||
maxMonsters: 20
|
||||
--- !u!4 &600231664
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -66531,9 +66387,7 @@ MonoBehaviour:
|
|||
m_Script: {fileID: 11500000, guid: 9eb5b341f7503494e9b0f88cc4b1c17d, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
healthSource: {fileID: 0}
|
||||
hpFillImage: {fileID: 0}
|
||||
hpText: {fileID: 0}
|
||||
targetObject: {fileID: 0}
|
||||
--- !u!1001 &603601734
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -68621,11 +68475,6 @@ Transform:
|
|||
m_CorrespondingSourceObject: {fileID: 4012526298001850, guid: 1b1f948f132c1cc429df46b11756f4a2, type: 3}
|
||||
m_PrefabInstance: {fileID: 619812415}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!4 &620355262 stripped
|
||||
Transform:
|
||||
m_CorrespondingSourceObject: {fileID: 1852576806548013000, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
m_PrefabInstance: {fileID: 1101415473}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!1 &620650552
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -115738,7 +115587,7 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
boss: {fileID: 358757531}
|
||||
fogWall: {fileID: 0}
|
||||
entranceWall: {fileID: 0}
|
||||
--- !u!65 &1081393078
|
||||
BoxCollider:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -119033,105 +118882,6 @@ Transform:
|
|||
m_CorrespondingSourceObject: {fileID: 4062256764471868, guid: 6c3328059b63be44f96b3571e2691511, type: 3}
|
||||
m_PrefabInstance: {fileID: 1101322778}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!1001 &1101415473
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 2
|
||||
m_Modification:
|
||||
serializedVersion: 3
|
||||
m_TransformParent: {fileID: 0}
|
||||
m_Modifications:
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalScale.x
|
||||
value: 1.8233726
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalScale.y
|
||||
value: 1.8233726
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalScale.z
|
||||
value: 1.8233726
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
value: 27.059
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalPosition.y
|
||||
value: 5.48
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalPosition.z
|
||||
value: 33.461
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalRotation.w
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalRotation.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalRotation.y
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalRotation.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.y
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_Name
|
||||
value: BossMonster 3D model
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5866666021909216657, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_Enabled
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5866666021909216657, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_Controller
|
||||
value:
|
||||
objectReference: {fileID: 9100000, guid: b207531287111e74d890e730f7278dfa, type: 2}
|
||||
- target: {fileID: 5866666021909216657, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_ApplyRootMotion
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents: []
|
||||
m_RemovedGameObjects: []
|
||||
m_AddedGameObjects:
|
||||
- targetCorrespondingSourceObject: {fileID: 1852576806548013000, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 1325714516}
|
||||
m_AddedComponents:
|
||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 358757533}
|
||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 358757532}
|
||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 358757531}
|
||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 358757530}
|
||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 358757536}
|
||||
m_SourcePrefab: {fileID: 100100000, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
--- !u!1001 &1101521501
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -134213,6 +133963,28 @@ PrefabInstance:
|
|||
m_AddedGameObjects: []
|
||||
m_AddedComponents: []
|
||||
m_SourcePrefab: {fileID: 100100000, guid: 26a3cbfd6906bd74e848cf2c8b765e85, type: 3}
|
||||
--- !u!1 &1231090171 stripped
|
||||
GameObject:
|
||||
m_CorrespondingSourceObject: {fileID: 5696671955299564549, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
m_PrefabInstance: {fileID: 7311604695488881352}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!4 &1231090172 stripped
|
||||
Transform:
|
||||
m_CorrespondingSourceObject: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
m_PrefabInstance: {fileID: 7311604695488881352}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!114 &1231090178
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1231090171}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: c23a9c1b76724ab459bf32a732f6a2f7, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
--- !u!1001 &1231957954
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -143010,38 +142782,6 @@ Transform:
|
|||
m_CorrespondingSourceObject: {fileID: 4708095961488610, guid: 01d14f7c3a49b4f4e86caee63ef815b1, type: 3}
|
||||
m_PrefabInstance: {fileID: 1323788198}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!1 &1325714515
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1325714516}
|
||||
m_Layer: 12
|
||||
m_Name: handPos
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &1325714516
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1325714515}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: -0.038344428, y: 0.70765644, z: -0.0024401005, w: -0.7055112}
|
||||
m_LocalPosition: {x: 1.304071, y: 1.8877394, z: -1.5619158}
|
||||
m_LocalScale: {x: 234.34999, y: 234.35004, z: 234.35002}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 1550577166}
|
||||
m_Father: {fileID: 620355262}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1001 &1326407535
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -165685,111 +165425,6 @@ Transform:
|
|||
m_CorrespondingSourceObject: {fileID: 4822900260135478, guid: 75fdec6c596df2b44b567836ba593557, type: 3}
|
||||
m_PrefabInstance: {fileID: 1548942974}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!1 &1550577165
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1550577166}
|
||||
- component: {fileID: 1550577169}
|
||||
- component: {fileID: 1550577168}
|
||||
- component: {fileID: 1550577167}
|
||||
m_Layer: 12
|
||||
m_Name: SilverBall
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &1550577166
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1550577165}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0.002655249, y: -0.03898664, z: -0.72380346, w: -0.68889886}
|
||||
m_LocalPosition: {x: 0.00886, y: -0.00478, z: 0.0054}
|
||||
m_LocalScale: {x: 0.0055073895, y: 0.0055073905, z: 0.0055073895}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1325714516}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!135 &1550577167
|
||||
SphereCollider:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1550577165}
|
||||
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: 3
|
||||
m_Radius: 0.5
|
||||
m_Center: {x: 0, y: 0, z: 0}
|
||||
--- !u!23 &1550577168
|
||||
MeshRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1550577165}
|
||||
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_RenderingLayerMask: 1
|
||||
m_RendererPriority: 0
|
||||
m_Materials:
|
||||
- {fileID: 2100000, guid: aea925dd0ba5758438a4e26ced28244e, 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_SortingLayerID: 0
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 0
|
||||
m_AdditionalVertexStreams: {fileID: 0}
|
||||
--- !u!33 &1550577169
|
||||
MeshFilter:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1550577165}
|
||||
m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0}
|
||||
--- !u!1001 &1551121841
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -187825,6 +187460,11 @@ Transform:
|
|||
m_CorrespondingSourceObject: {fileID: 4062256764471868, guid: 6c3328059b63be44f96b3571e2691511, type: 3}
|
||||
m_PrefabInstance: {fileID: 1759213612}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!1 &1760701545 stripped
|
||||
GameObject:
|
||||
m_CorrespondingSourceObject: {fileID: 7767661558022019919, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
|
||||
m_PrefabInstance: {fileID: 374250731514228064}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!1001 &1762494340
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -204619,10 +204259,10 @@ MonoBehaviour:
|
|||
m_Script: {fileID: 11500000, guid: 1f525fb5022b0754b9c5e1d725f8b2a4, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
spawnRange: 15
|
||||
respawnCooldown: 3
|
||||
mobTag: Run Monster
|
||||
optimizationRange: 60
|
||||
monsterPrefabs: []
|
||||
spawnPoints: []
|
||||
spawnInterval: 5
|
||||
maxMonsters: 20
|
||||
--- !u!4 &1927423250
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -219436,10 +219076,10 @@ MonoBehaviour:
|
|||
m_Script: {fileID: 11500000, guid: 1f525fb5022b0754b9c5e1d725f8b2a4, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
spawnRange: 15
|
||||
respawnCooldown: 3
|
||||
mobTag: Kamikaze
|
||||
optimizationRange: 60
|
||||
monsterPrefabs: []
|
||||
spawnPoints: []
|
||||
spawnInterval: 5
|
||||
maxMonsters: 20
|
||||
--- !u!4 &2067270637
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -224983,7 +224623,21 @@ MonoBehaviour:
|
|||
m_Script: {fileID: 11500000, guid: cecfa58891603f842944dcc723b41699, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
optimizationDistance: 40
|
||||
maxHP: 100
|
||||
attackDamage: 10
|
||||
expReward: 10
|
||||
moveSpeed: 3.5
|
||||
myWeapon: {fileID: 0}
|
||||
Monster_Idle: Monster_Idle
|
||||
Monster_GetDamage: Monster_GetDamage
|
||||
Monster_Die: Monster_Die
|
||||
attackRestDuration: 1.5
|
||||
hitSound: {fileID: 0}
|
||||
deathSound: {fileID: 0}
|
||||
deathEffectPrefab: {fileID: 0}
|
||||
hitEffect: {fileID: 0}
|
||||
impactSpawnPoint: {fileID: 0}
|
||||
--- !u!1001 &2112985138
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -227924,6 +227578,63 @@ GameObject:
|
|||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!1001 &374250731514228064
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 2
|
||||
m_Modification:
|
||||
serializedVersion: 3
|
||||
m_TransformParent: {fileID: 0}
|
||||
m_Modifications:
|
||||
- target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
value: 29.463804
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
|
||||
propertyPath: m_LocalPosition.y
|
||||
value: 6.54
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
|
||||
propertyPath: m_LocalPosition.z
|
||||
value: 37.02
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
|
||||
propertyPath: m_LocalRotation.w
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
|
||||
propertyPath: m_LocalRotation.x
|
||||
value: -0.00000005819078
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
|
||||
propertyPath: m_LocalRotation.y
|
||||
value: -0.00000007397057
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
|
||||
propertyPath: m_LocalRotation.z
|
||||
value: -0.000000045624613
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.y
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7767661558022019919, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
|
||||
propertyPath: m_Name
|
||||
value: SilverBall (1)
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents: []
|
||||
m_RemovedGameObjects: []
|
||||
m_AddedGameObjects: []
|
||||
m_AddedComponents: []
|
||||
m_SourcePrefab: {fileID: 100100000, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
|
||||
--- !u!224 &510035933647342717
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -228755,6 +228466,10 @@ PrefabInstance:
|
|||
serializedVersion: 3
|
||||
m_TransformParent: {fileID: 0}
|
||||
m_Modifications:
|
||||
- target: {fileID: -903076858900737616, guid: c34da37720b95c84887eda34e2d90e5b, type: 3}
|
||||
propertyPath: bossTransform
|
||||
value:
|
||||
objectReference: {fileID: 1231090172}
|
||||
- target: {fileID: 265854990890279069, guid: c34da37720b95c84887eda34e2d90e5b, type: 3}
|
||||
propertyPath: m_Name
|
||||
value: Player
|
||||
|
|
@ -229245,6 +228960,235 @@ MonoBehaviour:
|
|||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!1001 &7311604695488881352
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 2
|
||||
m_Modification:
|
||||
serializedVersion: 3
|
||||
m_TransformParent: {fileID: 0}
|
||||
m_Modifications:
|
||||
- target: {fileID: 4766546730822684, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 944323988545650569, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 1649737928517973471, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 1906678252579283472, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 2349772954066482596, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 2368488738584937952, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 2632517516497060400, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3140071916763302165, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3214020809470585975, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3917557170397923783, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4051200230809419435, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4148014776267224279, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4295377773018066043, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4531760650702335315, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4742553587330777874, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4895228312472489901, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
value: 27.059
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_LocalPosition.y
|
||||
value: 5.48
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_LocalPosition.z
|
||||
value: 33.461
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_LocalRotation.w
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_LocalRotation.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_LocalRotation.y
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_LocalRotation.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.y
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533461296662355806, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5696671955299564549, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Name
|
||||
value: BossMonster 3D model
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5696671955299564549, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5862204985775830713, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5992951552372829280, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6230346118947057135, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6491536537420366651, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7102369222656777545, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: config
|
||||
value:
|
||||
objectReference: {fileID: 11400000, guid: a7ebef2d438cdef42a54a32e572e2421, type: 2}
|
||||
- target: {fileID: 7102369222656777545, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: OnCounterActivated.m_PersistentCalls.m_Calls.Array.size
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7102369222656777545, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: OnHabitChangeRewarded.m_PersistentCalls.m_Calls.Array.size
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7102369222656777545, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: OnCounterActivated.m_PersistentCalls.m_Calls.Array.data[0].m_CallState
|
||||
value: 2
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7102369222656777545, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: OnHabitChangeRewarded.m_PersistentCalls.m_Calls.Array.data[0].m_CallState
|
||||
value: 2
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7230287829579350873, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7377015375545003093, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7511188995001642512, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7515041959607683961, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7607217621834580820, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7779629221345539216, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 8151139788885434819, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 8207281218213573469, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 8370141904445535276, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 8424341814146284104, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 8431478448715994143, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 9002269045746146953, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 9056329910564997272, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 9108673629704904850, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: ironBall
|
||||
value:
|
||||
objectReference: {fileID: 1760701545}
|
||||
- target: {fileID: 9109463353651491833, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 14
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents: []
|
||||
m_RemovedGameObjects:
|
||||
- {fileID: 7803200391641535762, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
m_AddedGameObjects: []
|
||||
m_AddedComponents:
|
||||
- targetCorrespondingSourceObject: {fileID: 5696671955299564549, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 1231090178}
|
||||
m_SourcePrefab: {fileID: 100100000, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
|
||||
--- !u!1001 &7449230931284885269
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -229434,9 +229378,7 @@ MonoBehaviour:
|
|||
m_Script: {fileID: 11500000, guid: 9eb5b341f7503494e9b0f88cc4b1c17d, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
healthSource: {fileID: 1432447526}
|
||||
hpFillImage: {fileID: 4391954558244079021}
|
||||
hpText: {fileID: 7128939526209516821}
|
||||
targetObject: {fileID: 0}
|
||||
--- !u!224 &8997534549618499257
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -229489,6 +229431,7 @@ SceneRoots:
|
|||
- {fileID: 321682814584122314}
|
||||
- {fileID: 1824697940}
|
||||
- {fileID: 85056390}
|
||||
- {fileID: 374250731514228064}
|
||||
- {fileID: 1175521096951328089}
|
||||
- {fileID: 600231664}
|
||||
- {fileID: 1927423250}
|
||||
|
|
@ -229499,6 +229442,6 @@ SceneRoots:
|
|||
- {fileID: 198901434}
|
||||
- {fileID: 929255795}
|
||||
- {fileID: 1844125250}
|
||||
- {fileID: 1101415473}
|
||||
- {fileID: 7311604695488881352}
|
||||
- {fileID: 1081393079}
|
||||
- {fileID: 48215575}
|
||||
|
|
|
|||
284
Assets/1.myPrefab/MyMonster/BossMonster 3D model.prefab
Normal file
284
Assets/1.myPrefab/MyMonster/BossMonster 3D model.prefab
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &1906678252579283472
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 6707239907687147596}
|
||||
m_Layer: 12
|
||||
m_Name: handPos
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &6707239907687147596
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1906678252579283472}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: -0.038344428, y: 0.70765644, z: -0.0024401005, w: -0.7055112}
|
||||
m_LocalPosition: {x: -0.035, y: 0.139, z: 0.001}
|
||||
m_LocalScale: {x: 234.34999, y: 234.35004, z: 234.35002}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 6519631083874139804}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1001 &4886349692092432724
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 2
|
||||
m_Modification:
|
||||
serializedVersion: 3
|
||||
m_TransformParent: {fileID: 0}
|
||||
m_Modifications:
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalScale.x
|
||||
value: 1.8233726
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalScale.y
|
||||
value: 1.8233726
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalScale.z
|
||||
value: 1.8233726
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
value: 27.059
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalPosition.y
|
||||
value: 5.48
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalPosition.z
|
||||
value: 33.461
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalRotation.w
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalRotation.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalRotation.y
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalRotation.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.y
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_Name
|
||||
value: BossMonster 3D model
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5866666021909216657, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_Enabled
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5866666021909216657, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_Controller
|
||||
value:
|
||||
objectReference: {fileID: 9100000, guid: b207531287111e74d890e730f7278dfa, type: 2}
|
||||
- target: {fileID: 5866666021909216657, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
propertyPath: m_ApplyRootMotion
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents: []
|
||||
m_RemovedGameObjects: []
|
||||
m_AddedGameObjects:
|
||||
- targetCorrespondingSourceObject: {fileID: 1852576806548013000, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 6707239907687147596}
|
||||
m_AddedComponents:
|
||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 9077611284751873490}
|
||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 2677911960108179361}
|
||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 9108673629704904850}
|
||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 7102369222656777545}
|
||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 3377266925740425433}
|
||||
m_SourcePrefab: {fileID: 100100000, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
--- !u!1 &5696671955299564549 stripped
|
||||
GameObject:
|
||||
m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
m_PrefabInstance: {fileID: 4886349692092432724}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!195 &9077611284751873490
|
||||
NavMeshAgent:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5696671955299564549}
|
||||
m_Enabled: 1
|
||||
m_AgentTypeID: 0
|
||||
m_Radius: 0.5
|
||||
m_Speed: 3.5
|
||||
m_Acceleration: 8
|
||||
avoidancePriority: 50
|
||||
m_AngularSpeed: 120
|
||||
m_StoppingDistance: 0
|
||||
m_AutoTraverseOffMeshLink: 1
|
||||
m_AutoBraking: 1
|
||||
m_AutoRepath: 1
|
||||
m_Height: 2
|
||||
m_BaseOffset: 0
|
||||
m_WalkableMask: 4294967295
|
||||
m_ObstacleAvoidanceType: 4
|
||||
--- !u!136 &2677911960108179361
|
||||
CapsuleCollider:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5696671955299564549}
|
||||
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: 2
|
||||
m_Radius: 0.5
|
||||
m_Height: 2.02
|
||||
m_Direction: 1
|
||||
m_Center: {x: 0, y: 0.9, z: 0}
|
||||
--- !u!114 &9108673629704904850
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5696671955299564549}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 04f39bf969074e6488f84b47bd517dc9, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
optimizationDistance: 40
|
||||
maxHP: 100
|
||||
attackDamage: 10
|
||||
expReward: 10
|
||||
moveSpeed: 3.5
|
||||
myWeapon: {fileID: 0}
|
||||
Monster_Idle: Monster_Idle
|
||||
Monster_GetDamage: Monster_GetDamage
|
||||
Monster_Die: Monster_Die
|
||||
attackRestDuration: 1.5
|
||||
hitSound: {fileID: 0}
|
||||
deathSound: {fileID: 0}
|
||||
deathEffectPrefab: {fileID: 0}
|
||||
hitEffect: {fileID: 0}
|
||||
impactSpawnPoint: {fileID: 0}
|
||||
counterSystem: {fileID: 7102369222656777545}
|
||||
patternInterval: 3
|
||||
attackRange: 3
|
||||
ironBall: {fileID: 0}
|
||||
handHolder: {fileID: 6707239907687147596}
|
||||
bossHealthBar: {fileID: 0}
|
||||
anim_Roar: Roar
|
||||
anim_Idle: Monster_Idle
|
||||
anim_Walk: Monster_Walk
|
||||
anim_Pickup: Skill_Pickup
|
||||
anim_Throw: Attack_Throw
|
||||
anim_DashReady: Skill_Dash_Ready
|
||||
anim_DashGo: Skill_Dash_Go
|
||||
anim_SmashCharge: Skill_Smash_Charge
|
||||
anim_SmashImpact: Skill_Smash_Impact
|
||||
anim_Sweep: Skill_Sweep
|
||||
debugState: "\uB300\uAE30 \uC911"
|
||||
--- !u!114 &7102369222656777545
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5696671955299564549}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5e36870a705f4fd44923bc5704c4a93b, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
config: {fileID: 11400000, guid: 4c6a2993f687f65409c4ece370872444, type: 2}
|
||||
usePersistence: 1
|
||||
OnCounterUpdated:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnCounterActivated:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnCounterDeactivated:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnHabitChangeRewarded:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
--- !u!54 &3377266925740425433
|
||||
Rigidbody:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5696671955299564549}
|
||||
serializedVersion: 4
|
||||
m_Mass: 1
|
||||
m_Drag: 0
|
||||
m_AngularDrag: 0.05
|
||||
m_CenterOfMass: {x: 0, y: 0, z: 0}
|
||||
m_InertiaTensor: {x: 1, y: 1, z: 1}
|
||||
m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ImplicitCom: 1
|
||||
m_ImplicitTensor: 1
|
||||
m_UseGravity: 1
|
||||
m_IsKinematic: 1
|
||||
m_Interpolate: 0
|
||||
m_Constraints: 80
|
||||
m_CollisionDetection: 0
|
||||
--- !u!4 &6519631083874139804 stripped
|
||||
Transform:
|
||||
m_CorrespondingSourceObject: {fileID: 1852576806548013000, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||
m_PrefabInstance: {fileID: 4886349692092432724}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d600b7cf8cd5668488dc657bfb881e96
|
||||
guid: fa6d549ce18d8bb479c3660d83e704d3
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1001 &3382000909673495095
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 2
|
||||
m_Modification:
|
||||
serializedVersion: 3
|
||||
m_TransformParent: {fileID: 0}
|
||||
m_Modifications:
|
||||
- target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
value: 28.749569
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
propertyPath: m_LocalPosition.y
|
||||
value: 5.692
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
propertyPath: m_LocalPosition.z
|
||||
value: 33.23644
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
propertyPath: m_LocalRotation.w
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
propertyPath: m_LocalRotation.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
propertyPath: m_LocalRotation.y
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
propertyPath: m_LocalRotation.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.y
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 919132149155446097, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
propertyPath: m_Name
|
||||
value: Mesh_0
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents: []
|
||||
m_RemovedGameObjects: []
|
||||
m_AddedGameObjects: []
|
||||
m_AddedComponents:
|
||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 7262612340710800498}
|
||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 4046718751856758141}
|
||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 390961823920064902}
|
||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 7314625792715952847}
|
||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 4718319889433233023}
|
||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 431623032249840988}
|
||||
m_SourcePrefab: {fileID: 100100000, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
--- !u!1 &2462956869760567142 stripped
|
||||
GameObject:
|
||||
m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
m_PrefabInstance: {fileID: 3382000909673495095}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!95 &7262612340710800498
|
||||
Animator:
|
||||
serializedVersion: 5
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2462956869760567142}
|
||||
m_Enabled: 1
|
||||
m_Avatar: {fileID: 9000000, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
|
||||
m_Controller: {fileID: 9100000, guid: b207531287111e74d890e730f7278dfa, type: 2}
|
||||
m_CullingMode: 0
|
||||
m_UpdateMode: 0
|
||||
m_ApplyRootMotion: 0
|
||||
m_LinearVelocityBlending: 0
|
||||
m_StabilizeFeet: 0
|
||||
m_WarningMessage:
|
||||
m_HasTransformHierarchy: 1
|
||||
m_AllowConstantClipSamplingOptimization: 1
|
||||
m_KeepAnimatorStateOnDisable: 0
|
||||
m_WriteDefaultValuesOnDisable: 0
|
||||
--- !u!54 &4046718751856758141
|
||||
Rigidbody:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2462956869760567142}
|
||||
serializedVersion: 4
|
||||
m_Mass: 1
|
||||
m_Drag: 0
|
||||
m_AngularDrag: 0.05
|
||||
m_CenterOfMass: {x: 0, y: 0, z: 0}
|
||||
m_InertiaTensor: {x: 1, y: 1, z: 1}
|
||||
m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ImplicitCom: 1
|
||||
m_ImplicitTensor: 1
|
||||
m_UseGravity: 1
|
||||
m_IsKinematic: 0
|
||||
m_Interpolate: 0
|
||||
m_Constraints: 80
|
||||
m_CollisionDetection: 0
|
||||
--- !u!195 &390961823920064902
|
||||
NavMeshAgent:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2462956869760567142}
|
||||
m_Enabled: 1
|
||||
m_AgentTypeID: 0
|
||||
m_Radius: 0.5
|
||||
m_Speed: 3.5
|
||||
m_Acceleration: 8
|
||||
avoidancePriority: 50
|
||||
m_AngularSpeed: 120
|
||||
m_StoppingDistance: 0
|
||||
m_AutoTraverseOffMeshLink: 1
|
||||
m_AutoBraking: 1
|
||||
m_AutoRepath: 1
|
||||
m_Height: 2
|
||||
m_BaseOffset: 0
|
||||
m_WalkableMask: 4294967295
|
||||
m_ObstacleAvoidanceType: 4
|
||||
--- !u!136 &7314625792715952847
|
||||
CapsuleCollider:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2462956869760567142}
|
||||
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: 2
|
||||
m_Radius: 0.5
|
||||
m_Height: 2.12
|
||||
m_Direction: 1
|
||||
m_Center: {x: 0, y: 0.93, z: 0}
|
||||
--- !u!114 &4718319889433233023
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2462956869760567142}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 04f39bf969074e6488f84b47bd517dc9, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
optimizationDistance: 40
|
||||
maxHP: 100
|
||||
attackDamage: 10
|
||||
expReward: 10
|
||||
moveSpeed: 3.5
|
||||
myWeapon: {fileID: 0}
|
||||
Monster_Idle: Monster_Idle
|
||||
Monster_GetDamage: Monster_GetDamage
|
||||
Monster_Die: Monster_Die
|
||||
attackRestDuration: 1.5
|
||||
hitSound: {fileID: 0}
|
||||
deathSound: {fileID: 0}
|
||||
deathEffectPrefab: {fileID: 0}
|
||||
hitEffect: {fileID: 0}
|
||||
impactSpawnPoint: {fileID: 0}
|
||||
counterSystem: {fileID: 0}
|
||||
patternInterval: 3
|
||||
ironBall: {fileID: 0}
|
||||
handHolder: {fileID: 0}
|
||||
bossHealthBar: {fileID: 0}
|
||||
--- !u!114 &431623032249840988
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2462956869760567142}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5e36870a705f4fd44923bc5704c4a93b, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
config: {fileID: 11400000, guid: 4c6a2993f687f65409c4ece370872444, type: 2}
|
||||
OnCounterActivated:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnCounterDeactivated:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnCounterPatternSelected:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnHabitChangeRewarded:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
bossPatterns:
|
||||
- patternName: Smash
|
||||
baseWeight: 30
|
||||
counterType: 1
|
||||
subCounterType: 0
|
||||
- patternName: DashAttack
|
||||
baseWeight: 30
|
||||
counterType: 2
|
||||
subCounterType: 0
|
||||
- patternName: ShieldWall
|
||||
baseWeight: 20
|
||||
counterType: 3
|
||||
subCounterType: 0
|
||||
|
|
@ -1,5 +1,31 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1102 &-734848756713148517
|
||||
AnimatorState:
|
||||
serializedVersion: 6
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Skill_Pickup
|
||||
m_Speed: 1
|
||||
m_CycleOffset: 0
|
||||
m_Transitions: []
|
||||
m_StateMachineBehaviours: []
|
||||
m_Position: {x: 50, y: 50, z: 0}
|
||||
m_IKOnFeet: 0
|
||||
m_WriteDefaultValues: 1
|
||||
m_Mirror: 0
|
||||
m_SpeedParameterActive: 0
|
||||
m_MirrorParameterActive: 0
|
||||
m_CycleOffsetParameterActive: 0
|
||||
m_TimeParameterActive: 0
|
||||
m_Motion: {fileID: 1827226128182048838, guid: db245789d27145b4abbed35593cbaad9, type: 3}
|
||||
m_Tag:
|
||||
m_SpeedParameter:
|
||||
m_MirrorParameter:
|
||||
m_CycleOffsetParameter:
|
||||
m_TimeParameter:
|
||||
--- !u!91 &9100000
|
||||
AnimatorController:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -8,13 +34,7 @@ AnimatorController:
|
|||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: BossMonsterAnimater
|
||||
serializedVersion: 5
|
||||
m_AnimatorParameters:
|
||||
- m_Name: Speed
|
||||
m_Type: 1
|
||||
m_DefaultFloat: 0
|
||||
m_DefaultInt: 0
|
||||
m_DefaultBool: 0
|
||||
m_Controller: {fileID: 9100000}
|
||||
m_AnimatorParameters: []
|
||||
m_AnimatorLayers:
|
||||
- serializedVersion: 5
|
||||
m_Name: Base Layer
|
||||
|
|
@ -285,7 +305,7 @@ AnimatorStateMachine:
|
|||
m_Position: {x: 530, y: 500, z: 0}
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: 6315225014600423063}
|
||||
m_Position: {x: 310, y: 320, z: 0}
|
||||
m_Position: {x: 300, y: 320, z: 0}
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: 2976953331190317660}
|
||||
m_Position: {x: 530, y: 440, z: 0}
|
||||
|
|
@ -301,6 +321,9 @@ AnimatorStateMachine:
|
|||
- serializedVersion: 1
|
||||
m_State: {fileID: 2223377946697074049}
|
||||
m_Position: {x: 320, y: 440, z: 0}
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: -734848756713148517}
|
||||
m_Position: {x: 320, y: 550, z: 0}
|
||||
m_ChildStateMachines: []
|
||||
m_AnyStateTransitions: []
|
||||
m_EntryTransitions: []
|
||||
|
|
|
|||
122
Assets/1.myPrefab/MyMonster/Monster Weapon/SilverBall (1).prefab
Normal file
122
Assets/1.myPrefab/MyMonster/Monster Weapon/SilverBall (1).prefab
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &7767661558022019919
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 3799945176027843355}
|
||||
- component: {fileID: 2802573740909485063}
|
||||
- component: {fileID: 3397712377444290059}
|
||||
- component: {fileID: 5299568709773687803}
|
||||
- component: {fileID: 6858471677023048159}
|
||||
m_Layer: 12
|
||||
m_Name: SilverBall (1)
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &3799945176027843355
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7767661558022019919}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: -0.00000005819078, y: -0.00000007397057, z: -0.000000045624613, w: 1}
|
||||
m_LocalPosition: {x: 29.463804, y: 6.54, z: 37.02}
|
||||
m_LocalScale: {x: 2.35335, y: 2.35335, z: 2.35335}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!33 &2802573740909485063
|
||||
MeshFilter:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7767661558022019919}
|
||||
m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0}
|
||||
--- !u!23 &3397712377444290059
|
||||
MeshRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7767661558022019919}
|
||||
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_RenderingLayerMask: 1
|
||||
m_RendererPriority: 0
|
||||
m_Materials:
|
||||
- {fileID: 2100000, guid: aea925dd0ba5758438a4e26ced28244e, 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_SortingLayerID: 0
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 0
|
||||
m_AdditionalVertexStreams: {fileID: 0}
|
||||
--- !u!135 &5299568709773687803
|
||||
SphereCollider:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7767661558022019919}
|
||||
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: 3
|
||||
m_Radius: 0.5
|
||||
m_Center: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &6858471677023048159
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7767661558022019919}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 8aab89a77c422e841ab70b85a3a041eb, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
damage: 20
|
||||
targetTag: Player
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4c6a2993f687f65409c4ece370872444
|
||||
NativeFormatImporter:
|
||||
guid: bef6d4b496fcaf94ea9cd20c3ba24d78
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
BIN
Assets/5.Enemy Animation/BossAnimation'/줍는모션 3_Unreal5.6.fbx
Normal file
BIN
Assets/5.Enemy Animation/BossAnimation'/줍는모션 3_Unreal5.6.fbx
Normal file
Binary file not shown.
|
|
@ -0,0 +1,113 @@
|
|||
fileFormatVersion: 2
|
||||
guid: db245789d27145b4abbed35593cbaad9
|
||||
ModelImporter:
|
||||
serializedVersion: 22200
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
materials:
|
||||
materialImportMode: 2
|
||||
materialName: 0
|
||||
materialSearch: 1
|
||||
materialLocation: 1
|
||||
animations:
|
||||
legacyGenerateAnimations: 4
|
||||
bakeSimulation: 0
|
||||
resampleCurves: 1
|
||||
optimizeGameObjects: 0
|
||||
removeConstantScaleCurves: 0
|
||||
motionNodeName:
|
||||
rigImportErrors:
|
||||
rigImportWarnings:
|
||||
animationImportErrors:
|
||||
animationImportWarnings:
|
||||
animationRetargetingWarnings:
|
||||
animationDoRetargetingWarnings: 0
|
||||
importAnimatedCustomProperties: 0
|
||||
importConstraints: 0
|
||||
animationCompression: 3
|
||||
animationRotationError: 0.5
|
||||
animationPositionError: 0.5
|
||||
animationScaleError: 0.5
|
||||
animationWrapMode: 0
|
||||
extraExposedTransformPaths: []
|
||||
extraUserProperties: []
|
||||
clipAnimations: []
|
||||
isReadable: 0
|
||||
meshes:
|
||||
lODScreenPercentages:
|
||||
- 0.25
|
||||
- 0.125
|
||||
- 0.0625
|
||||
- 0.01
|
||||
globalScale: 1
|
||||
meshCompression: 0
|
||||
addColliders: 0
|
||||
useSRGBMaterialColor: 1
|
||||
sortHierarchyByName: 1
|
||||
importPhysicalCameras: 1
|
||||
importVisibility: 1
|
||||
importBlendShapes: 1
|
||||
importCameras: 1
|
||||
importLights: 1
|
||||
nodeNameCollisionStrategy: 1
|
||||
fileIdsGeneration: 2
|
||||
swapUVChannels: 0
|
||||
generateSecondaryUV: 0
|
||||
useFileUnits: 1
|
||||
keepQuads: 0
|
||||
weldVertices: 1
|
||||
bakeAxisConversion: 0
|
||||
preserveHierarchy: 0
|
||||
skinWeightsMode: 0
|
||||
maxBonesPerVertex: 4
|
||||
minBoneWeight: 0.001
|
||||
optimizeBones: 1
|
||||
meshOptimizationFlags: -1
|
||||
indexFormat: 0
|
||||
secondaryUVAngleDistortion: 8
|
||||
secondaryUVAreaDistortion: 15.000001
|
||||
secondaryUVHardAngle: 88
|
||||
secondaryUVMarginMethod: 1
|
||||
secondaryUVMinLightmapResolution: 40
|
||||
secondaryUVMinObjectScale: 1
|
||||
secondaryUVPackMargin: 4
|
||||
useFileScale: 1
|
||||
strictVertexDataChecks: 0
|
||||
tangentSpace:
|
||||
normalSmoothAngle: 60
|
||||
normalImportMode: 0
|
||||
tangentImportMode: 3
|
||||
normalCalculationMode: 4
|
||||
legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
|
||||
blendShapeNormalImportMode: 1
|
||||
normalSmoothingSource: 0
|
||||
referencedClips: []
|
||||
importAnimation: 1
|
||||
humanDescription:
|
||||
serializedVersion: 3
|
||||
human: []
|
||||
skeleton: []
|
||||
armTwist: 0.5
|
||||
foreArmTwist: 0.5
|
||||
upperLegTwist: 0.5
|
||||
legTwist: 0.5
|
||||
armStretch: 0.05
|
||||
legStretch: 0.05
|
||||
feetSpacing: 0
|
||||
globalScale: 1
|
||||
rootMotionBoneName:
|
||||
hasTranslationDoF: 0
|
||||
hasExtraRoot: 0
|
||||
skeletonHasParents: 1
|
||||
lastHumanDescriptionAvatarSource: {instanceID: 0}
|
||||
autoGenerateAvatarMappingIfUnspecified: 1
|
||||
animationType: 3
|
||||
humanoidOversampling: 1
|
||||
avatarSetup: 1
|
||||
addHumanoidExtraRootOnlyWhenUsingAvatar: 1
|
||||
importBlendShapeDeformPercent: 1
|
||||
remapMaterialsIfMaterialImportModeIsNone: 0
|
||||
additionalBone: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Core/Utils.meta
Normal file
8
Assets/Scripts/Core/Utils.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 1f15c116f1cf57042a361cf23f1fb1d3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
41
Assets/Scripts/Core/Utils/AnimationLengthCach.cs
Normal file
41
Assets/Scripts/Core/Utils/AnimationLengthCach.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
using System.Collections.Generic; // 딕셔너리를 사용할거에요 -> System.Collections.Generic을
|
||||
|
||||
/// <summary>
|
||||
/// 애니메이션 클립 길이를 런타임에 캐싱하고 조회하는 유틸리티
|
||||
/// </summary>
|
||||
public class AnimationLengthCache // 클래스를 선언할거에요 -> 애니메이션 길이 캐싱을 담당하는 AnimationLengthCache를
|
||||
{
|
||||
private readonly Animator _animator; // 변수를 선언할거에요 -> 애니메이터 참조를 저장할 _animator를
|
||||
private readonly Dictionary<string, float> _clipLengthCache = new Dictionary<string, float>(); // 변수를 선언할거에요 -> 클립 이름과 길이를 저장할 캐시를
|
||||
|
||||
public AnimationLengthCache(Animator animator) // 생성자를 만들거에요 -> 애니메이터를 받는
|
||||
{
|
||||
_animator = animator; // 값을 저장할거에요 -> 전달받은 애니메이터를
|
||||
CacheAllClipLengths(); // 함수를 실행할거에요 -> 모든 클립 길이를 미리 저장하는
|
||||
}
|
||||
|
||||
private void CacheAllClipLengths() // 함수를 선언할거에요 -> 초기 캐싱 로직을
|
||||
{
|
||||
_clipLengthCache.Clear(); // 비울거에요 -> 기존 데이터를
|
||||
if (_animator == null || _animator.runtimeAnimatorController == null) return; // 조건이 맞으면 중단할거에요 -> 애니메이터가 없으면
|
||||
|
||||
foreach (AnimationClip clip in _animator.runtimeAnimatorController.animationClips) // 반복할거에요 -> 모든 클립을
|
||||
{
|
||||
if (!_clipLengthCache.ContainsKey(clip.name)) // 조건이 맞으면 실행할거에요 -> 캐시에 없는 이름이라면
|
||||
_clipLengthCache[clip.name] = clip.length; // 값을 저장할거에요 -> 길이를
|
||||
}
|
||||
}
|
||||
|
||||
public float GetClipLength(string clipName, float fallback = 1.0f) // 함수를 선언할거에요 -> 길이를 조회하는 GetClipLength를
|
||||
{
|
||||
if (_clipLengthCache.TryGetValue(clipName, out float length)) return length; // 조건이 맞으면 반환할거에요 -> 캐시에서 찾은 길이를
|
||||
|
||||
if (_animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 (비상용)
|
||||
{
|
||||
AnimatorStateInfo state = _animator.GetCurrentAnimatorStateInfo(0); // 정보를 가져올거에요 -> 현재 상태를
|
||||
if (state.IsName(clipName)) return state.length; // 조건이 맞으면 반환할거에요 -> 현재 상태의 길이를
|
||||
}
|
||||
return fallback; // 반환할거에요 -> 기본값을 (못 찾았을 때)
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Core/Utils/AnimationLengthCach.cs.meta
Normal file
11
Assets/Scripts/Core/Utils/AnimationLengthCach.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a03df9f7a6ef3b549a573abc7664a8af
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
24
Assets/Scripts/Core/Utils/ProjectileMath.cs
Normal file
24
Assets/Scripts/Core/Utils/ProjectileMath.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
|
||||
public static class ProjectileMath // 정적 클래스를 선언할거에요 -> 수학 계산용 ProjectileMath를
|
||||
{
|
||||
public static Vector3 CalculateLobVelocity(Vector3 origin, Vector3 target, float angle) // 함수를 선언할거에요 -> 곡사 속도를 계산하는
|
||||
{
|
||||
Vector3 dir = target - origin; // 벡터를 계산할거에요 -> 방향과 거리를
|
||||
float height = dir.y; // 값을 저장할거에요 -> 높이 차이를
|
||||
dir.y = 0; // 값을 초기화할거에요 -> 수평 거리 계산용으로 y를 0으로
|
||||
float dist = dir.magnitude; // 값을 계산할거에요 -> 수평 거리를
|
||||
|
||||
float a = angle * Mathf.Deg2Rad; // 값을 변환할거에요 -> 각도를 라디안으로
|
||||
dir.y = dist * Mathf.Tan(a); // 값을 계산할거에요 -> 각도에 따른 높이를
|
||||
dist += height / Mathf.Tan(a); // 값을 보정할거에요 -> 높이 차이에 따른 거리 보정을
|
||||
|
||||
float gravity = Physics.gravity.magnitude; // 값을 가져올거에요 -> 중력 가속도를
|
||||
float velocitySq = (gravity * dist * dist) / (2 * Mathf.Pow(Mathf.Cos(a), 2) * (dist * Mathf.Tan(a) - height)); // 값을 계산할거에요 -> 필요한 속도의 제곱을
|
||||
|
||||
if (velocitySq <= 0) return Vector3.zero; // 조건이 맞으면 반환할거에요 -> 계산 불가시 0을
|
||||
|
||||
float velocity = Mathf.Sqrt(velocitySq); // 값을 계산할거에요 -> 제곱근을 구해 실제 속도를
|
||||
return dir.normalized * velocity; // 값을 반환할거에요 -> 방향에 속도를 곱해서
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Core/Utils/ProjectileMath.cs.meta
Normal file
11
Assets/Scripts/Core/Utils/ProjectileMath.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b58ac9c02288aaf4698a6e64e77cdd53
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
55
Assets/Scripts/Core/Utils/StatusEffectProcessor.cs
Normal file
55
Assets/Scripts/Core/Utils/StatusEffectProcessor.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
using UnityEngine.AI; // 내비게이션 기능을 불러올거에요 -> UnityEngine.AI를
|
||||
using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
|
||||
|
||||
/// <summary>
|
||||
/// 화상, 독, 슬로우 등의 상태이상을 전담 처리하는 클래스
|
||||
/// </summary>
|
||||
public class StatusEffectProcessor // 클래스를 선언할거에요 -> 상태이상 로직을 담당하는 클래스를
|
||||
{
|
||||
private readonly MonoBehaviour _owner; // 변수를 선언할거에요 -> 코루틴 실행 주체를
|
||||
private readonly IDamageable _damageable; // 변수를 선언할거에요 -> 데미지 인터페이스를
|
||||
private readonly NavMeshAgent _agent; // 변수를 선언할거에요 -> 이동 에이전트를
|
||||
private readonly Animator _animator; // 변수를 선언할거에요 -> 애니메이터를
|
||||
|
||||
public StatusEffectProcessor(MonoBehaviour owner, IDamageable damageable, NavMeshAgent agent, Animator animator) // 생성자를 만들거에요 -> 의존성 주입을 위한
|
||||
{
|
||||
_owner = owner; // 값을 저장할거에요 -> 주인을
|
||||
_damageable = damageable; // 값을 저장할거에요 -> 데미지 대상을
|
||||
_agent = agent; // 값을 저장할거에요 -> 에이전트를
|
||||
_animator = animator; // 값을 저장할거에요 -> 애니메이터를
|
||||
}
|
||||
|
||||
public void ApplyBurn(float damage, float duration) => _owner.StartCoroutine(BurnRoutine(damage, duration)); // 함수를 선언할거에요 -> 화상 코루틴 시작을
|
||||
public void ApplySlow(float amount, float duration) => _owner.StartCoroutine(SlowRoutine(amount, duration)); // 함수를 선언할거에요 -> 슬로우 코루틴 시작을
|
||||
public void ApplyShock(float damage, float duration) { _damageable.TakeDamage(damage); _owner.StartCoroutine(StunRoutine(duration)); } // 함수를 선언할거에요 -> 충격과 스턴 시작을
|
||||
|
||||
private IEnumerator BurnRoutine(float damage, float duration) // 코루틴 함수를 정의할거에요 -> 화상 로직을
|
||||
{
|
||||
float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을
|
||||
while (elapsed < duration) // 반복할거에요 -> 지속 시간 동안
|
||||
{
|
||||
_damageable.TakeDamage(damage * 0.5f); // 실행할거에요 -> 0.5초치 데미지를
|
||||
yield return new WaitForSeconds(0.5f); // 기다릴거에요 -> 0.5초를
|
||||
elapsed += 0.5f; // 값을 더할거에요 -> 경과 시간에
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator SlowRoutine(float amount, float duration) // 코루틴 함수를 정의할거에요 -> 슬로우 로직을
|
||||
{
|
||||
if (_agent == null) yield break; // 조건이 맞으면 종료할거에요 -> 에이전트가 없으면
|
||||
float orgSpeed = _agent.speed; // 값을 저장할거에요 -> 원래 속도를
|
||||
_agent.speed *= (1f - Mathf.Clamp01(amount / 100f)); // 값을 바꿀거에요 -> 속도를 줄여서
|
||||
yield return new WaitForSeconds(duration); // 기다릴거에요 -> 지속 시간만큼
|
||||
_agent.speed = orgSpeed; // 값을 복구할거에요 -> 원래 속도로
|
||||
}
|
||||
|
||||
private IEnumerator StunRoutine(float duration) // 코루틴 함수를 정의할거에요 -> 스턴 로직을
|
||||
{
|
||||
if (_agent != null) _agent.isStopped = true; // 명령을 내릴거에요 -> 이동 정지를
|
||||
if (_animator != null) _animator.speed = 0; // 값을 바꿀거에요 -> 애니 속도를 0으로
|
||||
yield return new WaitForSeconds(duration); // 기다릴거에요 -> 스턴 시간만큼
|
||||
if (_agent != null && _agent.isOnNavMesh) _agent.isStopped = false; // 명령을 내릴거에요 -> 이동 재개를
|
||||
if (_animator != null) _animator.speed = 1; // 값을 바꿀거에요 -> 애니 속도 복구를
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Core/Utils/StatusEffectProcessor.cs.meta
Normal file
11
Assets/Scripts/Core/Utils/StatusEffectProcessor.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2e27a73f1d147004c9ec8e5309aa6f22
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,211 +1,80 @@
|
|||
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||
using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
|
||||
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
||||
using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
|
||||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
|
||||
using System.Collections.Generic; // 리스트를 사용할거에요 -> System.Collections.Generic을
|
||||
|
||||
/// <summary>
|
||||
/// 돌진 공격 몬스터
|
||||
/// </summary>
|
||||
public class ChargeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 ChargeMonster를
|
||||
public class ChargeMonster : MonsterClass // 클래스를 선언할거에요 -> 돌진 몬스터를
|
||||
{
|
||||
[Header("=== 돌진 공격 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 돌진 공격 설정 === 을
|
||||
[SerializeField] private float chargeSpeed = 15f; // 변수를 선언할거에요 -> 돌진 속도(15.0)를 chargeSpeed에
|
||||
[SerializeField] private float chargeRange = 10f; // 변수를 선언할거에요 -> 돌진 시작 거리(10.0)를 chargeRange에
|
||||
[SerializeField] private float chargeDuration = 2f; // 변수를 선언할거에요 -> 돌진 지속 시간(2.0초)을 chargeDuration에
|
||||
[SerializeField] private float chargeDelay = 3f; // 변수를 선언할거에요 -> 돌진 쿨타임(3.0초)을 chargeDelay에
|
||||
[SerializeField] private float prepareTime = 0.5f; // 변수를 선언할거에요 -> 돌진 준비 시간(0.5초)을 prepareTime에
|
||||
[Header("=== 돌진 설정 ===")] // 인스펙터 제목을 달거에요 -> 돌진 설정을
|
||||
[SerializeField] private float chargeSpeed = 15f; // 변수를 선언할거에요 -> 속도를
|
||||
[SerializeField] private float chargeRange = 10f; // 변수를 선언할거에요 -> 감지 거리를
|
||||
[SerializeField] private float chargeDuration = 2f; // 변수를 선언할거에요 -> 지속 시간을
|
||||
[SerializeField] private float chargeDelay = 3f; // 변수를 선언할거에요 -> 쿨타임을
|
||||
[SerializeField] private float prepareTime = 0.5f; // 변수를 선언할거에요 -> 준비 시간을
|
||||
|
||||
// 🎁 [추가] 드랍 아이템 리스트
|
||||
[Header("=== 드랍 아이템 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 드랍 아이템 설정 === 을
|
||||
[Tooltip("드랍 가능한 아이템 리스트 (랜덤 1개)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
[SerializeField] private List<GameObject> dropItemPrefabs; // 리스트를 선언할거에요 -> 드랍 아이템 프리팹 목록을 dropItemPrefabs에
|
||||
[Tooltip("아이템이 나올 확률 (0 ~ 100%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
[Range(0, 100)][SerializeField] private float dropChance = 30f; // 변수를 선언할거에요 -> 드랍 확률(30%)을 dropChance에
|
||||
[Header("애니메이션")] // 인스펙터 제목을 달거에요 -> 애니메이션을
|
||||
[SerializeField] private string chargeAnim = "Monster_Charge"; // 변수를 선언할거에요 -> 돌진 애니를
|
||||
[SerializeField] private string prepareAnim = "Monster_ChargePrepare"; // 변수를 선언할거에요 -> 준비 애니를
|
||||
[SerializeField] private string walkAnim = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니를
|
||||
|
||||
private float lastChargeTime; // 변수를 선언할거에요 -> 마지막 돌진 시간을 lastChargeTime에
|
||||
private bool isCharging = false; // 변수를 선언할거에요 -> 돌진 중 여부를 isCharging에
|
||||
private bool isPreparing = false; // 변수를 선언할거에요 -> 준비 중 여부를 isPreparing에
|
||||
private Vector3 chargeDirection; // 변수를 선언할거에요 -> 돌진 방향 벡터를 chargeDirection에
|
||||
private float lastChargeTime; // 변수를 선언할거에요 -> 마지막 돌진 시간을
|
||||
private bool isCharging; // 변수를 선언할거에요 -> 돌진 중 여부를
|
||||
private Rigidbody _rb; // 변수를 선언할거에요 -> 리지드바디를
|
||||
|
||||
[Header("공격 / 이동 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 공격 / 이동 애니메이션 을
|
||||
[SerializeField] private string chargeAnimation = "Monster_Charge"; // 변수를 선언할거에요 -> 돌진 애니메이션 이름을 chargeAnimation에
|
||||
[SerializeField] private string prepareAnimation = "Monster_ChargePrepare"; // 변수를 선언할거에요 -> 준비 애니메이션 이름을 prepareAnimation에
|
||||
[SerializeField] private string Monster_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 Monster_Walk에
|
||||
|
||||
[Header("AI 상세 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 상세 설정 을
|
||||
[SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에
|
||||
[SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 간격(2.0초)을 patrolInterval에
|
||||
|
||||
private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에
|
||||
private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어 감지 여부를 isPlayerInZone에
|
||||
private Rigidbody _rigidbody; // 변수를 선언할거에요 -> 물리 컴포넌트를 담을 _rigidbody를
|
||||
|
||||
protected override void Awake() // 함수를 덮어씌워 실행할거에요 -> Awake 초기화 로직을
|
||||
protected override void Awake() // 함수를 실행할거에요 -> 초기화 Awake를
|
||||
{
|
||||
base.Awake(); // 부모의 함수를 실행할거에요 -> MonsterClass의 Awake를
|
||||
_rigidbody = GetComponent<Rigidbody>(); // 컴포넌트를 가져올거에요 -> Rigidbody를
|
||||
if (_rigidbody == null) _rigidbody = gameObject.AddComponent<Rigidbody>(); // 조건이 맞으면 실행할거에요 -> 없으면 새로 추가해서 _rigidbody에
|
||||
_rigidbody.isKinematic = true; // 설정을 바꿀거에요 -> 물리 연산을 꺼두기(Kinematic)로
|
||||
base.Awake(); // 부모 함수를 실행할거에요
|
||||
_rb = GetComponent<Rigidbody>(); // 가져올거에요 -> 리지드바디를
|
||||
if (_rb == null) _rb = gameObject.AddComponent<Rigidbody>(); // 추가할거에요 -> 없으면 새로
|
||||
_rb.isKinematic = true; // 끌거에요 -> 물리 연산을
|
||||
}
|
||||
|
||||
protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기 설정을 Init에서
|
||||
protected override void Init() { if (agent != null) agent.stoppingDistance = 1f; } // 초기화할거에요 -> 정지 거리를
|
||||
|
||||
protected override void ExecuteAILogic() // 함수를 실행할거에요 -> AI 로직을
|
||||
{
|
||||
if (agent != null) agent.stoppingDistance = 1f; // 조건이 맞으면 설정할거에요 -> 에이전트 정지 거리를 1.0으로
|
||||
if (animator != null) animator.applyRootMotion = false; // 조건이 맞으면 설정할거에요 -> 애니메이션 이동을 끄기로
|
||||
if (isHit || isCharging || isAttacking) return; // 중단할거에요 -> 행동 불가면
|
||||
float dist = Vector3.Distance(transform.position, playerTransform.position); // 계산할거에요 -> 거리를
|
||||
if (dist <= chargeRange && Time.time >= lastChargeTime + chargeDelay) StartCoroutine(ChargeRoutine()); // 실행할거에요 -> 돌진을
|
||||
else ChasePlayer(); // 실행할거에요 -> 추격을
|
||||
}
|
||||
|
||||
protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을
|
||||
private void ChasePlayer() // 함수를 선언할거에요 -> 추격 로직을
|
||||
{
|
||||
if (isHit || isCharging || isPreparing) return; // 조건이 맞으면 중단할거에요 -> 피격, 돌진, 준비 중이라면
|
||||
|
||||
float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를
|
||||
|
||||
if (isPlayerInZone || distance <= chargeRange * 1.5f) // 조건이 맞으면 실행할거에요 -> 감지 구역 안이거나 거리가 가까우면
|
||||
{
|
||||
HandleChargeCombat(distance); // 함수를 실행할거에요 -> 전투 로직인 HandleChargeCombat을
|
||||
}
|
||||
else // 조건이 틀리면 실행할거에요 -> 멀리 있다면
|
||||
{
|
||||
Patrol(); // 함수를 실행할거에요 -> 순찰을 도는 Patrol을
|
||||
}
|
||||
|
||||
UpdateMovementAnimation(); // 함수를 실행할거에요 -> 이동 애니메이션을 갱신하는 UpdateMovementAnimation을
|
||||
if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(playerTransform.position); } // 이동할거에요 -> 플레이어에게
|
||||
animator.Play(walkAnim); // 재생할거에요 -> 걷기 애니를
|
||||
}
|
||||
|
||||
void HandleChargeCombat(float distance) // 함수를 선언할거에요 -> 거리 기반 전투 판단 로직 HandleChargeCombat을
|
||||
private IEnumerator ChargeRoutine() // 코루틴 함수를 정의할거에요 -> 돌진 과정을
|
||||
{
|
||||
if (distance <= chargeRange && Time.time >= lastChargeTime + chargeDelay) // 조건이 맞으면 실행할거에요 -> 사거리 안이고 쿨타임이 지났다면
|
||||
{
|
||||
StartCoroutine(PrepareCharge()); // 코루틴을 실행할거에요 -> 돌진 준비인 PrepareCharge를
|
||||
}
|
||||
else if (!isCharging && !isPreparing) // 조건이 맞으면 실행할거에요 -> 돌진이나 준비 중이 아니라면 (추격)
|
||||
{
|
||||
if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면
|
||||
{
|
||||
if (agent.isStopped) agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀라고
|
||||
agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로
|
||||
}
|
||||
}
|
||||
}
|
||||
isAttacking = true; agent.isStopped = true; // 설정할거에요 -> 공격 상태와 정지를
|
||||
animator.Play(prepareAnim); // 재생할거에요 -> 준비 애니를
|
||||
|
||||
IEnumerator PrepareCharge() // 코루틴 함수를 선언할거에요 -> 돌진 준비 과정인 PrepareCharge를
|
||||
{
|
||||
isPreparing = true; // 상태를 바꿀거에요 -> 준비 중 상태를 참(true)으로
|
||||
if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면
|
||||
{
|
||||
agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고
|
||||
agent.ResetPath(); // 명령을 내릴거에요 -> 경로를 초기화하라고
|
||||
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
|
||||
}
|
||||
|
||||
chargeDirection = (playerTransform.position - transform.position).normalized; // 벡터를 계산할거에요 -> 플레이어를 향하는 방향을
|
||||
chargeDirection.y = 0; // 값을 바꿀거에요 -> 높이 차이를 무시하게 y를 0으로
|
||||
if (chargeDirection != Vector3.zero) transform.rotation = Quaternion.LookRotation(chargeDirection); // 회전시킬거에요 -> 돌진 방향을 바라보게
|
||||
|
||||
if (!string.IsNullOrEmpty(prepareAnimation)) animator.Play(prepareAnimation, 0, 0f); // 재생할거에요 -> 준비 애니메이션을
|
||||
|
||||
Debug.Log("[ChargeMonster] 돌진 준비..."); // 로그를 출력할거에요 -> 준비 메시지를
|
||||
Vector3 dir = (playerTransform.position - transform.position).normalized; dir.y = 0; // 계산할거에요 -> 방향을
|
||||
transform.rotation = Quaternion.LookRotation(dir); // 회전할거에요 -> 방향대로
|
||||
yield return new WaitForSeconds(prepareTime); // 기다릴거에요 -> 준비 시간만큼
|
||||
|
||||
StartCoroutine(Charge()); // 코루틴을 실행할거에요 -> 실제 돌진인 Charge를
|
||||
}
|
||||
isCharging = true; lastChargeTime = Time.time; // 설정할거에요 -> 돌진 상태와 시간을
|
||||
agent.enabled = false; _rb.isKinematic = false; // 전환할거에요 -> 물리 이동으로
|
||||
animator.Play(chargeAnim); // 재생할거에요 -> 돌진 애니를
|
||||
|
||||
IEnumerator Charge() // 코루틴 함수를 선언할거에요 -> 실제 돌진 동작인 Charge를
|
||||
{
|
||||
isPreparing = false; // 상태를 바꿀거에요 -> 준비 상태를 거짓(false)으로
|
||||
isCharging = true; // 상태를 바꿀거에요 -> 돌진 중 상태를 참(true)으로
|
||||
lastChargeTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 돌진 시간으로
|
||||
|
||||
if (agent != null) agent.enabled = false; // 기능을 끌거에요 -> 길찾기 에이전트를 (물리 이동을 위해)
|
||||
if (_rigidbody != null) _rigidbody.isKinematic = false; // 기능을 켤거에요 -> 물리 연산을 (충돌 감지를 위해)
|
||||
|
||||
animator.Play(chargeAnimation, 0, 0f); // 재생할거에요 -> 돌진 애니메이션을
|
||||
Debug.Log("[ChargeMonster] 돌진 시작!"); // 로그를 출력할거에요 -> 돌진 시작 메시지를
|
||||
|
||||
float chargeStartTime = Time.time; // 값을 저장할거에요 -> 돌진 시작 시간을
|
||||
|
||||
while (Time.time < chargeStartTime + chargeDuration) // 반복할거에요 -> 지속 시간이 끝날 때까지
|
||||
float elapsed = 0f; // 초기화할거에요 -> 경과 시간을
|
||||
while (elapsed < chargeDuration) // 반복할거에요 -> 지속 시간 동안
|
||||
{
|
||||
if (_rigidbody != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
|
||||
_rigidbody.velocity = chargeDirection * chargeSpeed; // 값을 넣을거에요 -> 속도를 돌진 방향과 속도로
|
||||
else // 조건이 틀리면 실행할거에요 -> 리지드바디가 없다면 (비상용)
|
||||
transform.position += chargeDirection * chargeSpeed * Time.deltaTime; // 이동시킬거에요 -> 위치를 직접 수정해서
|
||||
_rb.velocity = transform.forward * chargeSpeed; // 적용할거에요 -> 속도를
|
||||
elapsed += Time.deltaTime; // 더할거에요 -> 시간을
|
||||
yield return null; // 대기할거에요 -> 다음 프레임까지
|
||||
}
|
||||
|
||||
if (_rigidbody != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
|
||||
{
|
||||
_rigidbody.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
|
||||
_rigidbody.isKinematic = true; // 기능을 끌거에요 -> 물리 연산을 (다시 NavMesh 이동을 위해)
|
||||
}
|
||||
|
||||
if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
|
||||
{
|
||||
agent.enabled = true; // 기능을 켤거에요 -> 길찾기 에이전트를
|
||||
agent.ResetPath(); // 초기화할거에요 -> 경로를
|
||||
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 내부 속도를 0으로
|
||||
}
|
||||
|
||||
isCharging = false; // 상태를 바꿀거에요 -> 돌진 중 상태를 거짓(false)으로
|
||||
Debug.Log("[ChargeMonster] 돌진 종료, 쿨타임 시작"); // 로그를 출력할거에요 -> 종료 메시지를
|
||||
_rb.velocity = Vector3.zero; _rb.isKinematic = true; agent.enabled = true; // 복구할거에요 -> 상태를
|
||||
isCharging = false; OnAttackEnd(); // 종료할거에요 -> 돌진과 공격을
|
||||
}
|
||||
|
||||
void UpdateMovementAnimation() // 함수를 선언할거에요 -> 애니메이션 갱신 함수 UpdateMovementAnimation을
|
||||
private void OnCollisionEnter(Collision col) // 함수를 실행할거에요 -> 충돌 시
|
||||
{
|
||||
if (isCharging || isPreparing || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면
|
||||
if (agent.enabled && agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 움직이고 있다면
|
||||
animator.Play(Monster_Walk); // 재생할거에요 -> 걷기 애니메이션을
|
||||
else // 조건이 틀리면 실행할거에요 -> 멈춰 있다면
|
||||
animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션을
|
||||
}
|
||||
|
||||
void Patrol() // 함수를 선언할거에요 -> 순찰 로직 Patrol을
|
||||
{
|
||||
if (Time.time < nextPatrolTime) return; // 조건이 맞으면 중단할거에요 -> 대기 시간이라면
|
||||
Vector3 randomPoint = transform.position + new Vector3( // 벡터를 계산할거에요 -> 랜덤 순찰 지점을
|
||||
Random.Range(-patrolRadius, patrolRadius),
|
||||
0,
|
||||
Random.Range(-patrolRadius, patrolRadius)
|
||||
);
|
||||
|
||||
if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> 유효한 NavMesh 위치라면
|
||||
if (agent.isOnNavMesh) agent.SetDestination(hit.position); // 명령을 내릴거에요 -> 그곳으로 이동하라고
|
||||
|
||||
nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을
|
||||
}
|
||||
|
||||
private void OnCollisionEnter(Collision collision) // 함수를 실행할거에요 -> 물리 충돌이 발생했을 때 OnCollisionEnter를
|
||||
{
|
||||
if (!isCharging) return; // 조건이 맞으면 중단할거에요 -> 돌진 중이 아니라면
|
||||
if (collision.gameObject.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 충돌한 대상이 플레이어라면
|
||||
if (isCharging && col.gameObject.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 돌진 중 플레이어 충돌이면
|
||||
{
|
||||
if (collision.gameObject.TryGetComponent<PlayerHealth>(out var playerHealth)) // 컴포넌트를 가져올거에요 -> 플레이어 체력 스크립트를
|
||||
{
|
||||
if (!playerHealth.isInvincible) // 조건이 맞으면 실행할거에요 -> 무적 상태가 아니라면
|
||||
playerHealth.TakeDamage(attackDamage); // 함수를 실행할거에요 -> 데미지를 입히는 TakeDamage를
|
||||
}
|
||||
var t = col.gameObject.GetComponent<IDamageable>(); // 가져올거에요 -> 인터페이스를
|
||||
if (t != null) t.TakeDamage(attackDamage); // 입힐거에요 -> 데미지를
|
||||
}
|
||||
}
|
||||
|
||||
// ☠️ [추가] 죽을 때 아이템 드랍
|
||||
protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 사망 시 아이템 드랍 로직인 OnDie를
|
||||
{
|
||||
if (_rigidbody != null) _rigidbody.velocity = Vector3.zero; // 값을 넣을거에요 -> 죽을 때 미끄러지지 않게 속도를 0으로
|
||||
|
||||
if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) // 조건이 맞으면 실행할거에요 -> 아이템 리스트가 있다면
|
||||
{
|
||||
float randomValue = Random.Range(0f, 100f); // 값을 뽑을거에요 -> 0~100 사이 랜덤값을
|
||||
if (randomValue <= dropChance) // 조건이 맞으면 실행할거에요 -> 확률에 당첨되었다면
|
||||
{
|
||||
int randomIndex = Random.Range(0, dropItemPrefabs.Count); // 값을 뽑을거에요 -> 랜덤 인덱스를
|
||||
if (dropItemPrefabs[randomIndex] != null) // 조건이 맞으면 실행할거에요 -> 아이템이 유효하다면
|
||||
{
|
||||
Instantiate(dropItemPrefabs[randomIndex], transform.position + Vector3.up * 0.5f, Quaternion.identity); // 생성할거에요 -> 아이템을 몬스터 위치에
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } // 상태를 바꿀거에요 -> 플레이어 감지 시 참(true)으로
|
||||
private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } // 상태를 바꿀거에요 -> 플레이어가 나가면 거짓(false)으로
|
||||
}
|
||||
|
|
@ -1,240 +1,95 @@
|
|||
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||
using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
|
||||
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
||||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
|
||||
|
||||
/// <summary>
|
||||
/// 자폭 몬스터 (Kamikaze)
|
||||
/// - 플레이어에게 전력 질주
|
||||
/// - 범위 내 진입 시 제자리에서 카운트다운 후 폭발
|
||||
/// </summary>
|
||||
public class ExplodeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 ExplodeMonster를
|
||||
public class ExplodeMonster : MonsterClass // 클래스를 선언할거에요 -> 자폭 몬스터를
|
||||
{
|
||||
[Header("=== 자폭 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 자폭 설정 === 을
|
||||
[SerializeField] private float explodeRange = 4f; // 변수를 선언할거에요 -> 폭발 데미지 반경(4.0)을 explodeRange에
|
||||
[SerializeField] private float triggerRange = 2.5f; // 변수를 선언할거에요 -> 자폭 시퀀스 시작 거리(2.5)를 triggerRange에
|
||||
[SerializeField] private float fuseTime = 1.5f; // 변수를 선언할거에요 -> 폭발 지연 시간(1.5초)을 fuseTime에
|
||||
[SerializeField] private float explosionDamage = 50f; // 변수를 선언할거에요 -> 폭발 데미지(50.0)를 explosionDamage에
|
||||
[Header("=== 자폭 설정 ===")] // 인스펙터 제목을 달거에요 -> 자폭 설정을
|
||||
[SerializeField] private float explodeRange = 4f; // 변수를 선언할거에요 -> 폭발 범위를
|
||||
[SerializeField] private float triggerRange = 2.5f; // 변수를 선언할거에요 -> 감지 범위를
|
||||
[SerializeField] private float fuseTime = 1.5f; // 변수를 선언할거에요 -> 지연 시간을
|
||||
[SerializeField] private float explosionDamage = 50f; // 변수를 선언할거에요 -> 폭발 데미지를
|
||||
|
||||
[Header("폭발 효과")] // 인스펙터 창에 제목을 표시할거에요 -> 폭발 효과 를
|
||||
[SerializeField] private GameObject explosionEffectPrefab; // 변수를 선언할거에요 -> 폭발 이펙트 프리팹을 explosionEffectPrefab에
|
||||
[SerializeField] private ParticleSystem fuseEffect; // 변수를 선언할거에요 -> 도화선(준비) 이펙트를 fuseEffect에
|
||||
[SerializeField] private AudioClip fuseSound; // 변수를 선언할거에요 -> 도화선 소리를 fuseSound에
|
||||
[SerializeField] private AudioClip explosionSound; // 변수를 선언할거에요 -> 폭발 소리를 explosionSound에
|
||||
[Header("이펙트")] // 인스펙터 제목을 달거에요 -> 이펙트를
|
||||
[SerializeField] private GameObject explosionEffectPrefab; // 변수를 선언할거에요 -> 폭발 이펙트를
|
||||
[SerializeField] private ParticleSystem fuseEffect; // 변수를 선언할거에요 -> 도화선 이펙트를
|
||||
[SerializeField] private AudioClip fuseSound; // 변수를 선언할거에요 -> 도화선 소리를
|
||||
[SerializeField] private AudioClip explosionSound; // 변수를 선언할거에요 -> 폭발 소리를
|
||||
|
||||
[Header("애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 애니메이션 을
|
||||
[SerializeField] private string runAnimation = "Monster_Run"; // 변수를 선언할거에요 -> 달리기 애니메이션 이름을 runAnimation에
|
||||
[SerializeField] private string fuseAnimation = "Monster_Fuse"; // 변수를 선언할거에요 -> 자폭 준비 애니메이션 이름을 fuseAnimation에
|
||||
[Header("애니메이션")] // 인스펙터 제목을 달거에요 -> 애니메이션을
|
||||
[SerializeField] private string runAnim = "Monster_Run"; // 변수를 선언할거에요 -> 달리기 애니 이름을
|
||||
[SerializeField] private string fuseAnim = "Monster_Fuse"; // 변수를 선언할거에요 -> 점화 애니 이름을
|
||||
|
||||
[Header("AI 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 설정 을
|
||||
[SerializeField] private float chaseSpeed = 6f; // 변수를 선언할거에요 -> 추격 속도(6.0)를 chaseSpeed에
|
||||
[SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에
|
||||
[SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 간격(2.0초)을 patrolInterval에
|
||||
private bool hasExploded = false; // 변수를 선언할거에요 -> 폭발 여부를
|
||||
|
||||
private bool isExploding = false; // 변수를 초기화할거에요 -> 폭발 진행 중 여부를 거짓(false)으로
|
||||
private bool hasExploded = false; // 변수를 초기화할거에요 -> 이미 터졌는지 여부를 거짓(false)으로
|
||||
private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에
|
||||
private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어 감지 여부를 isPlayerInZone에
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// 초기화
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을
|
||||
protected override void Init() // 함수를 실행할거에요 -> 초기화 Init을
|
||||
{
|
||||
if (agent != null)
|
||||
{
|
||||
agent.speed = chaseSpeed; // 값을 설정할거에요 -> 이동 속도를 추격 속도로
|
||||
agent.stoppingDistance = 0.5f; // 값을 설정할거에요 -> 정지 거리를 아주 짧게(0.5)
|
||||
}
|
||||
if (animator != null) animator.applyRootMotion = false; // 설정을 바꿀거에요 -> 애니메이션 이동을 끄기로
|
||||
|
||||
if (fuseEffect != null) fuseEffect.Stop(); // 기능을 실행할거에요 -> 도화선 이펙트가 켜져있다면 끄기를
|
||||
if (agent != null) agent.stoppingDistance = 0.5f; // 설정을 바꿀거에요 -> 정지 거리를 짧게
|
||||
}
|
||||
|
||||
protected override void OnResetStats() // 함수를 덮어씌워 실행할거에요 -> 스탯 초기화 로직인 OnResetStats를
|
||||
protected override void ExecuteAILogic() // 함수를 실행할거에요 -> AI 로직을
|
||||
{
|
||||
isExploding = false; // 상태를 바꿀거에요 -> 폭발 진행 상태를 거짓(false)으로
|
||||
hasExploded = false; // 상태를 바꿀거에요 -> 폭발 완료 상태를 거짓(false)으로
|
||||
if (fuseEffect != null) fuseEffect.Stop(); // 기능을 실행할거에요 -> 도화선 이펙트를 끄기를
|
||||
}
|
||||
if (isHit || isAttacking || hasExploded) return; // 조건이 맞으면 중단할거에요 -> 행동 불가면
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// AI 로직
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
float dist = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의
|
||||
|
||||
protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을
|
||||
{
|
||||
if (isHit || isExploding || hasExploded) return; // 조건이 맞으면 중단할거에요 -> 피격, 자폭 중, 이미 폭발 상태라면
|
||||
|
||||
float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를
|
||||
|
||||
// 플레이어가 감지 범위(15m) 안에 있거나 트리거에 닿았으면 추격
|
||||
if (isPlayerInZone || distance <= 15f) // 조건이 맞으면 실행할거에요 -> 추격 조건이 만족되면
|
||||
if (dist <= triggerRange) // 조건이 맞으면 실행할거에요 -> 감지 거리 안이면
|
||||
{
|
||||
ChasePlayer(distance); // 함수를 실행할거에요 -> 추격 및 자폭 처리를 하는 ChasePlayer를
|
||||
StartCoroutine(ExplodeRoutine()); // 코루틴을 실행할거에요 -> 자폭 시퀀스를
|
||||
}
|
||||
else // 조건이 틀리면 실행할거에요 -> 멀리 있다면
|
||||
else // 조건이 틀리면 실행할거에요 -> 멀면
|
||||
{
|
||||
Patrol(); // 함수를 실행할거에요 -> 순찰을 도는 Patrol을
|
||||
UpdateMovementAnimation(); // 함수를 실행할거에요 -> 애니메이션을 갱신하는 UpdateMovementAnimation을
|
||||
ChasePlayer(); // 함수를 실행할거에요 -> 추격을
|
||||
}
|
||||
}
|
||||
|
||||
void ChasePlayer(float distance) // 함수를 선언할거에요 -> 거리별 추격 행동을 정하는 ChasePlayer를
|
||||
private void ChasePlayer() // 함수를 선언할거에요 -> 추격 로직을
|
||||
{
|
||||
// 1. 폭발 시작 거리 안으로 들어왔다? -> 점화!
|
||||
if (distance <= triggerRange) // 조건이 맞으면 실행할거에요 -> 자폭 거리 이내라면
|
||||
if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 내비게이션 위라면
|
||||
{
|
||||
StartCoroutine(ExplodeRoutine()); // 코루틴을 실행할거에요 -> 자폭 시퀀스인 ExplodeRoutine을
|
||||
return; // 중단할거에요 -> 더 이상 이동하지 않도록
|
||||
}
|
||||
|
||||
// 2. 아직 멀었다? -> 전력 질주
|
||||
if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면
|
||||
{
|
||||
agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀라고
|
||||
agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로
|
||||
|
||||
// 달리기 애니메이션
|
||||
animator.Play(runAnimation); // 재생할거에요 -> 달리기 애니메이션을
|
||||
agent.isStopped = false; // 명령을 내릴거에요 -> 이동 재개
|
||||
agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 플레이어 위치로
|
||||
}
|
||||
animator.Play(runAnim); // 재생할거에요 -> 달리기 애니를
|
||||
}
|
||||
|
||||
void UpdateMovementAnimation() // 함수를 선언할거에요 -> 애니메이션 갱신 함수 UpdateMovementAnimation을
|
||||
private IEnumerator ExplodeRoutine() // 코루틴 함수를 정의할거에요 -> 자폭 과정을
|
||||
{
|
||||
if (isExploding || isHit) return; // 조건이 맞으면 중단할거에요 -> 폭발 중이거나 맞고 있다면
|
||||
isAttacking = true; // 상태를 바꿀거에요 -> 공격 중으로 (이동 불가)
|
||||
hasExploded = true; // 상태를 바꿀거에요 -> 폭발 됨으로 (중복 방지)
|
||||
|
||||
if (agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 움직이고 있다면
|
||||
animator.Play(runAnimation); // 재생할거에요 -> 달리기 애니메이션을
|
||||
else // 조건이 틀리면 실행할거에요 -> 멈춰 있다면
|
||||
animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션을
|
||||
if (agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 명령을 내릴거에요 -> 정지
|
||||
|
||||
// 1. 점화
|
||||
animator.Play(fuseAnim); // 재생할거에요 -> 점화 애니를
|
||||
if (fuseEffect != null) fuseEffect.Play(); // 실행할거에요 -> 도화선 효과를
|
||||
if (fuseSound != null) audioSource.PlayOneShot(fuseSound); // 재생할거에요 -> 도화선 소리를
|
||||
|
||||
yield return new WaitForSeconds(fuseTime); // 기다릴거에요 -> 지연 시간만큼
|
||||
|
||||
// 2. 폭발
|
||||
Explode(); // 함수를 실행할거에요 -> 폭발 처리를
|
||||
}
|
||||
|
||||
void Patrol() // 함수를 선언할거에요 -> 순찰 로직 Patrol을
|
||||
private void Explode() // 함수를 선언할거에요 -> 실제 폭발 로직을
|
||||
{
|
||||
if (Time.time < nextPatrolTime) return; // 조건이 맞으면 중단할거에요 -> 대기 시간이라면
|
||||
if (explosionEffectPrefab != null) Instantiate(explosionEffectPrefab, transform.position, Quaternion.identity); // 생성할거에요 -> 폭발 이펙트를
|
||||
if (explosionSound != null) AudioSource.PlayClipAtPoint(explosionSound, transform.position); // 재생할거에요 -> 폭발 소리를
|
||||
|
||||
Vector3 randomPoint = transform.position + new Vector3( // 벡터를 계산할거에요 -> 랜덤 순찰 지점을
|
||||
Random.Range(-patrolRadius, patrolRadius),
|
||||
0,
|
||||
Random.Range(-patrolRadius, patrolRadius)
|
||||
);
|
||||
|
||||
if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> 유효한 위치라면
|
||||
if (agent.isOnNavMesh) agent.SetDestination(hit.position); // 명령을 내릴거에요 -> 이동하라고
|
||||
|
||||
nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을
|
||||
}
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// 💣 폭발 시퀀스 (점화 -> 대기 -> 쾅!)
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
IEnumerator ExplodeRoutine() // 코루틴 함수를 선언할거에요 -> 자폭 진행 과정인 ExplodeRoutine을
|
||||
{
|
||||
if (hasExploded) yield break; // 조건이 맞으면 종료할거에요 -> 이미 터졌다면
|
||||
|
||||
isExploding = true; // 상태를 바꿀거에요 -> 폭발 진행 상태를 참(true)으로
|
||||
hasExploded = true; // 상태를 바꿀거에요 -> 폭발 완료 상태를 참(true)으로 (중복 방지)
|
||||
isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로 (부모 간섭 방지)
|
||||
|
||||
// ⭐ [핵심] 급브레이크! (문워크 방지)
|
||||
if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면
|
||||
// 범위 데미지 처리
|
||||
Collider[] hits = Physics.OverlapSphere(transform.position, explodeRange); // 배열에 담을거에요 -> 범위 내 충돌체들을
|
||||
foreach (var hit in hits) // 반복할거에요 -> 충돌체마다
|
||||
{
|
||||
agent.isStopped = true; // 명령을 내릴거에요 -> 멈추라고
|
||||
agent.ResetPath(); // 명령을 내릴거에요 -> 경로를 지우라고
|
||||
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
|
||||
}
|
||||
|
||||
Debug.Log($"💣 자폭 카운트다운! ({fuseTime}초 뒤 폭발)"); // 로그를 출력할거에요 -> 자폭 예고 메시지를
|
||||
|
||||
// 1. 부들부들 떨기 (폭발 준비 모션)
|
||||
if (!string.IsNullOrEmpty(fuseAnimation)) animator.Play(fuseAnimation); // 재생할거에요 -> 준비 애니메이션을
|
||||
|
||||
// 2. 치익~ 소리 & 이펙트
|
||||
if (fuseEffect != null) fuseEffect.Play(); // 실행할거에요 -> 도화선 이펙트를
|
||||
if (fuseSound != null && audioSource != null) audioSource.PlayOneShot(fuseSound); // 재생할거에요 -> 도화선 소리를
|
||||
|
||||
// 3. 도망갈 시간 주기
|
||||
yield return new WaitForSeconds(fuseTime); // 기다릴거에요 -> 폭발 지연 시간만큼
|
||||
|
||||
// 4. 쾅!
|
||||
PerformExplosion(); // 함수를 실행할거에요 -> 실제 폭발 처리를
|
||||
}
|
||||
|
||||
void PerformExplosion() // 함수를 선언할거에요 -> 폭발 데미지와 이펙트를 처리하는 PerformExplosion을
|
||||
{
|
||||
Debug.Log("💥💥💥 쾅!!!"); // 로그를 출력할거에요 -> 폭발 메시지를
|
||||
|
||||
// 폭발 이펙트 생성 (Particle System)
|
||||
if (explosionEffectPrefab != null) // 조건이 맞으면 실행할거에요 -> 폭발 프리팹이 있다면
|
||||
{
|
||||
Instantiate(explosionEffectPrefab, transform.position, Quaternion.identity); // 생성할거에요 -> 폭발 이펙트를 현재 위치에
|
||||
}
|
||||
|
||||
// 폭발음
|
||||
if (explosionSound != null) // 조건이 맞으면 실행할거에요 -> 폭발음이 있다면
|
||||
{
|
||||
AudioSource.PlayClipAtPoint(explosionSound, transform.position, 1f); // 재생할거에요 -> 폭발 소리를 해당 위치에서
|
||||
}
|
||||
|
||||
// 주변 데미지 처리
|
||||
DamageNearbyTargets(); // 함수를 실행할거에요 -> 주변 대상에게 데미지를 주는 DamageNearbyTargets를
|
||||
|
||||
// 몬스터 사망 처리 (MonsterClass 기능 사용)
|
||||
Die(); // 함수를 실행할거에요 -> 몬스터를 죽게 만드는 Die를
|
||||
}
|
||||
|
||||
void DamageNearbyTargets() // 함수를 선언할거에요 -> 폭발 범위 내 데미지를 입히는 DamageNearbyTargets를
|
||||
{
|
||||
// 폭발 범위(Sphere) 안에 있는 모든 물체 검사
|
||||
Collider[] hitColliders = Physics.OverlapSphere(transform.position, explodeRange); // 배열에 담을거에요 -> 폭발 범위 내의 충돌체들을
|
||||
|
||||
foreach (Collider hit in hitColliders) // 반복할거에요 -> 감지된 모든 충돌체에 대해
|
||||
{
|
||||
// 플레이어 데미지
|
||||
if (hit.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 대상이 플레이어라면
|
||||
if (hit.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 플레이어라면
|
||||
{
|
||||
if (hit.TryGetComponent<PlayerHealth>(out var playerHealth)) // 컴포넌트를 가져올거에요 -> 플레이어 체력 스크립트를
|
||||
{
|
||||
if (!playerHealth.isInvincible) // 조건이 맞으면 실행할거에요 -> 무적 상태가 아니라면
|
||||
{
|
||||
playerHealth.TakeDamage(explosionDamage); // 함수를 실행할거에요 -> 폭발 데미지를 입히는 TakeDamage를
|
||||
Debug.Log($"💥 플레이어 폭사! 데미지: {explosionDamage}"); // 로그를 출력할거에요 -> 피격 메시지를
|
||||
}
|
||||
}
|
||||
}
|
||||
// (선택사항) 주변 몬스터도 팀킬?
|
||||
else if (hit.CompareTag("Enemy") && hit.gameObject != gameObject) // 조건이 맞으면 실행할거에요 -> 대상이 다른 몬스터라면
|
||||
{
|
||||
// 팀킬 로직이 필요하면 여기에 추가
|
||||
var hp = hit.GetComponent<IDamageable>(); // 가져올거에요 -> 체력 인터페이스를
|
||||
if (hp != null) hp.TakeDamage(explosionDamage); // 실행할거에요 -> 폭발 데미지 입히기를
|
||||
}
|
||||
}
|
||||
|
||||
// 자폭이므로 몬스터 사망
|
||||
currentHP = 0; // 값을 설정할거에요 -> 체력을 0으로
|
||||
Die(); // 실행할거에요 -> 사망 함수를
|
||||
}
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// 유틸리티
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
// 자폭병은 때려도 폭발 안 멈춤 (취향에 따라 수정 가능)
|
||||
protected override void OnStartHit() { } // 함수를 비워둘거에요 -> 피격 시 아무 행동도 안 하게 (폭발 캔슬 방지)
|
||||
|
||||
// 죽을 때 퓨즈 끄기
|
||||
protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 사망 시 정리를 위해
|
||||
{
|
||||
if (fuseEffect != null && fuseEffect.isPlaying) fuseEffect.Stop(); // 기능을 실행할거에요 -> 도화선 이펙트가 켜져있다면 끄기를
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } // 상태를 바꿀거에요 -> 플레이어 진입 시 감지 상태를 참으로
|
||||
private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } // 상태를 바꿀거에요 -> 플레이어 이탈 시 감지 상태를 거짓으로
|
||||
|
||||
// 에디터에서 범위 보여주기
|
||||
private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 에디터에서 범위를 그리는 OnDrawGizmosSelected를
|
||||
{
|
||||
Gizmos.color = Color.red; // 색상을 설정할거에요 -> 감지 범위 색을 빨간색으로
|
||||
Gizmos.DrawWireSphere(transform.position, triggerRange); // 그림을 그릴거에요 -> 자폭 감지 범위를
|
||||
|
||||
Gizmos.color = new Color(1f, 0.5f, 0f, 0.3f); // 색상을 설정할거에요 -> 폭발 범위 색을 주황색 반투명으로
|
||||
Gizmos.DrawSphere(transform.position, explodeRange); // 그림을 그릴거에요 -> 폭발 데미지 범위를
|
||||
}
|
||||
protected override void OnStartHit() { } // 가상 함수를 비울거에요 -> 피격 되어도 멈추지 않게 (슈퍼아머)
|
||||
}
|
||||
|
|
@ -1,80 +1,28 @@
|
|||
using System.Collections.Generic; // 제네릭 컬렉션을 쓰기 위해 불러올거에요 -> System.Collections.Generic을
|
||||
using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
|
||||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
|
||||
public class MonsterWeapon : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 MonsterWeapon을
|
||||
{ // 코드 블록을 시작할거에요 -> MonsterWeapon 범위를
|
||||
public class MonsterWeapon : MonoBehaviour // 클래스를 선언할거에요 -> 일반 몬스터 무기를
|
||||
{
|
||||
[SerializeField] private float baseDamage = 10f; // 변수를 선언할거에요 -> 기본 데미지를
|
||||
private float finalDamage; // 변수를 선언할거에요 -> 최종 데미지를
|
||||
private BoxCollider col; // 변수를 선언할거에요 -> 콜라이더를
|
||||
|
||||
[Header("무기 설정")] // 인스펙터에 제목을 표시할거에요 -> 무기 설정을
|
||||
[Tooltip("이 무기 고유의 공격력 (예: 10)")] // 인스펙터에 툴팁을 달거에요 -> weaponBaseDamage 설명을
|
||||
[SerializeField] private float weaponBaseDamage = 10f; // 변수를 선언할거에요 -> 무기 기본 공격력을 10으로
|
||||
private void Awake() // 함수를 실행할거에요 -> 초기화 Awake를
|
||||
{
|
||||
col = GetComponent<BoxCollider>(); // 가져올거에요 -> 박스 콜라이더를
|
||||
finalDamage = baseDamage; // 설정할거에요 -> 초기 데미지를
|
||||
EnableHitBox(); // 실행할거에요 -> 일단 켜두기를
|
||||
}
|
||||
|
||||
private float _finalDamage; // 변수를 선언할거에요 -> 최종 데미지를 저장할 _finalDamage를
|
||||
[SerializeField] private BoxCollider _weaponCollider; // 변수를 선언할거에요 -> 무기 콜라이더를 저장할 _weaponCollider를
|
||||
public void SetDamage(float amount) => finalDamage = amount; // 함수를 선언할거에요 -> 데미지 설정을
|
||||
public void EnableHitBox() { if (col != null) col.enabled = true; } // 함수를 선언할거에요 -> 판정 켜기를
|
||||
public void DisableHitBox() { if (col != null) col.enabled = false; } // 함수를 선언할거에요 -> 판정 끄기를
|
||||
|
||||
private void Awake() // 함수를 선언할거에요 -> 시작 시 1번 실행되는 Awake를
|
||||
{ // 코드 블록을 시작할거에요 -> Awake 범위를
|
||||
_weaponCollider = GetComponent<BoxCollider>(); // 컴포넌트를 가져올거에요 -> 내 오브젝트의 BoxCollider를 찾아 _weaponCollider에
|
||||
_finalDamage = weaponBaseDamage; // 값을 넣을거에요 -> 최종 데미지를 기본 데미지로 초기화
|
||||
//DisableHitBox(); // 주석 처리할거에요 -> 시작 시 판정을 끄는 코드(현재는 안 씀)
|
||||
EnableHitBox(); // 함수를 실행할거에요 -> 시작하자마자 판정을 켜기(현재 설정)
|
||||
} // 코드 블록을 끝낼거에요 -> Awake를
|
||||
|
||||
public void SetDamage(float monsterStrength) // 함수를 선언할거에요 -> 몬스터 스탯을 받아 최종 데미지를 세팅하는 SetDamage를
|
||||
{ // 코드 블록을 시작할거에요 -> SetDamage 범위를
|
||||
_finalDamage = weaponBaseDamage + monsterStrength; // 값을 계산할거에요 -> 기본 데미지 + 몬스터 힘으로 최종 데미지 설정
|
||||
Debug.Log($"✅ [무기] 데미지 설정됨: 총 {_finalDamage}"); // 로그를 찍을거에요 -> 최종 데미지가 얼마인지
|
||||
} // 코드 블록을 끝낼거에요 -> SetDamage를
|
||||
|
||||
public void EnableHitBox() // 함수를 선언할거에요 -> 공격 판정을 켜는 EnableHitBox를
|
||||
{ // 코드 블록을 시작할거에요 -> EnableHitBox 범위를
|
||||
Debug.Log("enabletest"); // 로그를 찍을거에요 -> 함수가 호출됐는지 확인용
|
||||
|
||||
if (_weaponCollider != null) // 조건을 검사할거에요 -> 콜라이더가 정상적으로 잡혔는지
|
||||
{ // 코드 블록을 시작할거에요 -> 콜라이더가 있을 때 처리
|
||||
Debug.Log("setcollider"); // 로그를 찍을거에요 -> 콜라이더 enable 직전 확인용
|
||||
_weaponCollider.enabled = true; // 값을 바꿀거에요 -> 트리거 판정을 켜기
|
||||
Debug.Log("🔴 [무기] 공격 판정 ON! (휘두르기 시작)"); // 로그를 찍을거에요 -> 판정이 켜졌음을
|
||||
} // 코드 블록을 끝낼거에요 -> 콜라이더 처리
|
||||
} // 코드 블록을 끝낼거에요 -> EnableHitBox를
|
||||
|
||||
public void DisableHitBox() // 함수를 선언할거에요 -> 공격 판정을 끄는 DisableHitBox를
|
||||
{ // 코드 블록을 시작할거에요 -> DisableHitBox 범위를
|
||||
if (_weaponCollider != null) // 조건을 검사할거에요 -> 콜라이더가 있는지
|
||||
{ // 코드 블록을 시작할거에요 -> 콜라이더가 있을 때 처리
|
||||
_weaponCollider.enabled = false; // 값을 바꿀거에요 -> 트리거 판정을 끄기
|
||||
Debug.Log("⚪ [무기] 공격 판정 OFF (휘두르기 끝)"); // 로그를 찍을거에요 -> 판정이 꺼졌음을
|
||||
} // 코드 블록을 끝낼거에요 -> 콜라이더 처리
|
||||
} // 코드 블록을 끝낼거에요 -> DisableHitBox를
|
||||
|
||||
// ⭐ 여기가 핵심! 닿는 모든 것을 기록함 // 설명을 적을거에요 -> 트리거 충돌 감지 핵심 구간임을
|
||||
private void OnTriggerEnter(Collider other) // 함수를 선언할거에요 -> 트리거에 다른 콜라이더가 들어오면 호출되는 OnTriggerEnter를
|
||||
{ // 코드 블록을 시작할거에요 -> OnTriggerEnter 범위를
|
||||
// 1. 무엇이든 닿으면 일단 로그를 찍음 (범인 색출) // 설명을 적을거에요 -> 뭐랑 부딪히는지 디버깅용
|
||||
Debug.Log($"💥 [충돌 감지] 무기가 닿은 것: {other.name} / 태그: {other.tag}"); // 로그를 찍을거에요 -> 충돌한 오브젝트 이름/태그를
|
||||
|
||||
if (other.CompareTag("Player")) // 조건을 검사할거에요 -> 닿은 대상이 Player 태그인지
|
||||
{ // 코드 블록을 시작할거에요 -> 플레이어일 때 처리
|
||||
PlayerHealth playerHealth = other.GetComponent<PlayerHealth>(); // 컴포넌트를 가져올거에요 -> 플레이어의 PlayerHealth를 찾아 playerHealth에
|
||||
|
||||
if (playerHealth != null) // 조건을 검사할거에요 -> PlayerHealth가 실제로 붙어있는지
|
||||
{ // 코드 블록을 시작할거에요 -> PlayerHealth가 있을 때 처리
|
||||
if (!playerHealth.isInvincible) // 조건을 검사할거에요 -> 플레이어가 무적인지 아닌지
|
||||
{ // 코드 블록을 시작할거에요 -> 무적이 아닐 때 처리
|
||||
playerHealth.TakeDamage(_finalDamage); // 함수를 실행할거에요 -> 플레이어에게 최종 데미지 적용
|
||||
Debug.Log($"⚔️ [적중] 플레이어 체력 깎임! (-{_finalDamage})"); // 로그를 찍을거에요 -> 데미지가 들어갔음을
|
||||
} // 코드 블록을 끝낼거에요 -> 무적 아닐 때 처리
|
||||
else // 조건이 아니면 실행할거에요 -> 무적이면
|
||||
{ // 코드 블록을 시작할거에요 -> 무적일 때 처리
|
||||
Debug.Log("🛡️ [방어] 플레이어가 무적 상태입니다."); // 로그를 찍을거에요 -> 무적이라 데미지 안 들어감을
|
||||
} // 코드 블록을 끝낼거에요 -> 무적 처리
|
||||
|
||||
DisableHitBox(); // 함수를 실행할거에요 -> 한 번 맞추면 바로 판정 끄기(중복 타격 방지)
|
||||
} // 코드 블록을 끝낼거에요 -> PlayerHealth 있을 때 처리
|
||||
else // 조건이 아니면 실행할거에요 -> PlayerHealth가 없으면
|
||||
{ // 코드 블록을 시작할거에요 -> 오류 처리
|
||||
Debug.LogError("⚠️ [오류] 플레이어 태그는 있는데 'PlayerHealth' 스크립트가 없습니다!"); // 에러 로그를 찍을거에요 -> 컴포넌트 누락 경고
|
||||
} // 코드 블록을 끝낼거에요 -> 오류 처리
|
||||
} // 코드 블록을 끝낼거에요 -> 플레이어 처리
|
||||
} // 코드 블록을 끝낼거에요 -> OnTriggerEnter를
|
||||
|
||||
} // 코드 블록을 끝낼거에요 -> MonsterWeapon을
|
||||
private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 충돌 감지를
|
||||
{
|
||||
if (other.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 플레이어라면
|
||||
{
|
||||
var hp = other.GetComponent<IDamageable>(); // 가져올거에요 -> 체력 인터페이스를
|
||||
if (hp != null) { hp.TakeDamage(finalDamage); DisableHitBox(); } // 실행할거에요 -> 데미지를 주고 판정을 끄기를
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,359 +1,193 @@
|
|||
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||
using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
|
||||
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
||||
using System; // 기본 시스템 기능(Action 등)을 사용할거에요 -> System을
|
||||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
using UnityEngine.AI; // 길찾기 기능을 불러올거에요 -> UnityEngine.AI를
|
||||
using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
|
||||
using System; // 시스템 기능을 사용할거에요 -> System을
|
||||
|
||||
/// <summary>
|
||||
/// 몬스터 기본 클래스 (공통 기능만)
|
||||
/// - 공격 방식은 자식 클래스에서 구현
|
||||
/// </summary>
|
||||
public abstract class MonsterClass : MonoBehaviour, IDamageable // 추상 클래스를 선언할거에요 -> MonoBehaviour와 IDamageable을 상속받는 MonsterClass를
|
||||
public abstract class MonsterClass : MonoBehaviour, IDamageable // 추상 클래스를 선언할거에요 -> MonsterClass를
|
||||
{
|
||||
[Header("--- 최적화 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 최적화 설정 --- 을
|
||||
protected Renderer mobRenderer; // 변수를 선언할거에요 -> 몬스터의 렌더러(외형) 컴포넌트를 담을 mobRenderer를
|
||||
protected Transform playerTransform; // 변수를 선언할거에요 -> 플레이어의 위치 정보를 담을 playerTransform을
|
||||
[SerializeField] protected float optimizationDistance = 40f; // 변수를 선언할거에요 -> 몬스터 최적화(AI 정지) 거리(40.0)를 optimizationDistance에
|
||||
[Header("--- 최적화 ---")] // 인스펙터 제목을 달거에요 -> 최적화 설정을
|
||||
protected Renderer mobRenderer; // 변수를 선언할거에요 -> 렌더러를
|
||||
protected Transform playerTransform; // 변수를 선언할거에요 -> 플레이어 위치를
|
||||
[SerializeField] protected float optimizationDistance = 40f; // 변수를 선언할거에요 -> 최적화 거리를
|
||||
|
||||
[Header("몬스터 기본 스탯")] // 인스펙터 창에 제목을 표시할거에요 -> 몬스터 기본 스탯 을
|
||||
[SerializeField] protected float maxHP = 100f; // 변수를 선언할거에요 -> 최대 체력(100.0)을 maxHP에
|
||||
[SerializeField] protected float attackDamage = 10f; // 변수를 선언할거에요 -> 공격력(10.0)을 attackDamage에
|
||||
[SerializeField] protected int expReward = 10; // 변수를 선언할거에요 -> 처치 시 경험치 보상(10)을 expReward에
|
||||
[SerializeField] protected float moveSpeed = 3.5f; // 변수를 선언할거에요 -> 이동 속도(3.5)를 moveSpeed에
|
||||
[Header("스탯")] // 인스펙터 제목을 달거에요 -> 스탯 설정을
|
||||
[SerializeField] protected float maxHP = 100f; // 변수를 선언할거에요 -> 최대 체력을
|
||||
[SerializeField] protected float attackDamage = 10f; // 변수를 선언할거에요 -> 공격력을
|
||||
[SerializeField] protected int expReward = 10; // 변수를 선언할거에요 -> 경험치 보상을
|
||||
[SerializeField] protected float moveSpeed = 3.5f; // 변수를 선언할거에요 -> 이동 속도를
|
||||
|
||||
protected float currentHP; // 변수를 선언할거에요 -> 현재 체력을 저장할 currentHP를
|
||||
public event Action<float, float> OnHealthChanged; // 이벤트를 선언할거에요 -> 체력이 변할 때 알릴 OnHealthChanged를
|
||||
protected float currentHP; // 변수를 선언할거에요 -> 현재 체력을
|
||||
public event Action<float, float> OnHealthChanged; // 이벤트를 선언할거에요 -> 체력 변경 알림을
|
||||
|
||||
[Header("전투 / 무기 (선택사항)")] // 인스펙터 창에 제목을 표시할거에요 -> 전투 / 무기 (선택사항) 을
|
||||
// ⭐ 근접 몬스터가 사용할 무기 (원거리 몬스터는 비워둬도 됨)
|
||||
[SerializeField] protected MonsterWeapon myWeapon; // 변수를 선언할거에요 -> 몬스터가 장착한 무기 스크립트를 myWeapon에
|
||||
[Header("전투")] // 인스펙터 제목을 달거에요 -> 전투 설정을
|
||||
[SerializeField] protected MonsterWeapon myWeapon; // 변수를 선언할거에요 -> 무기를
|
||||
|
||||
[Header("피격 / 사망 / 대기 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 피격 / 사망 / 대기 애니메이션 을
|
||||
[SerializeField] protected string Monster_Idle = "Monster_Idle"; // 변수를 선언할거에요 -> 대기 애니메이션 이름("Monster_Idle")을 Monster_Idle에
|
||||
[SerializeField] protected string Monster_GetDamage = "Monster_GetDamage"; // 변수를 선언할거에요 -> 피격 애니메이션 이름("Monster_GetDamage")을 Monster_GetDamage에
|
||||
[SerializeField] protected string Monster_Die = "Monster_Die"; // 변수를 선언할거에요 -> 사망 애니메이션 이름("Monster_Die")을 Monster_Die에
|
||||
[Header("애니메이션")] // 인스펙터 제목을 달거에요 -> 애니메이션을
|
||||
[SerializeField] protected string Monster_Idle = "Monster_Idle"; // 변수를 선언할거에요 -> 대기 애니 이름을
|
||||
[SerializeField] protected string Monster_GetDamage = "Monster_GetDamage"; // 변수를 선언할거에요 -> 피격 애니 이름을
|
||||
[SerializeField] protected string Monster_Die = "Monster_Die"; // 변수를 선언할거에요 -> 사망 애니 이름을
|
||||
|
||||
protected Animator animator; // 변수를 선언할거에요 -> 애니메이션 제어 컴포넌트를 담을 animator를
|
||||
protected NavMeshAgent agent; // 변수를 선언할거에요 -> 길찾기 에이전트 컴포넌트를 담을 agent를
|
||||
protected AudioSource audioSource; // 변수를 선언할거에요 -> 소리 재생 컴포넌트를 담을 audioSource를
|
||||
protected Animator animator; // 변수를 선언할거에요 -> 애니메이터를
|
||||
protected NavMeshAgent agent; // 변수를 선언할거에요 -> 에이전트를
|
||||
protected AudioSource audioSource; // 변수를 선언할거에요 -> 오디오 소스를
|
||||
protected StatusEffectProcessor statusProcessor; // 변수를 선언할거에요 -> 상태이상 처리기를
|
||||
|
||||
// ⭐ [핵심] 자식 클래스에서 공유할 상태 변수들 (중복 선언 방지)
|
||||
protected bool isHit;
|
||||
protected bool isDead;
|
||||
protected bool isAttacking;
|
||||
protected bool isHit, isDead, isAttacking; // 상태 변수들을 선언할거에요
|
||||
public bool IsAggroed { get; protected set; } // 프로퍼티를 선언할거에요 -> 어그로 여부를
|
||||
|
||||
public bool IsAggroed { get; protected set; } // 프로퍼티를 선언할거에요 -> 어그로(전투) 상태 여부를 외부에서 읽기만 가능하게 IsAggroed에
|
||||
[Header("AI")] // 인스펙터 제목을 달거에요 -> AI 설정을
|
||||
[SerializeField] protected float attackRestDuration = 1.5f; // 변수를 선언할거에요 -> 공격 후 휴식 시간을
|
||||
protected bool isResting; // 변수를 선언할거에요 -> 휴식 여부를
|
||||
|
||||
[Header("AI 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 설정 을
|
||||
[SerializeField] protected float attackRestDuration = 1.5f; // 변수를 선언할거에요 -> 공격 후 대기 시간(1.5초)을 attackRestDuration에
|
||||
protected bool isResting; // 변수를 선언할거에요 -> 휴식 중인지 여부를 저장할 isResting을
|
||||
public static Action<int> OnMonsterKilled; // 이벤트를 선언할거에요 -> 사망 전역 알림을
|
||||
|
||||
public static System.Action<int> OnMonsterKilled; // 정적 이벤트를 선언할거에요 -> 몬스터 처치 시 경험치를 전달할 OnMonsterKilled를
|
||||
[Header("사운드/이펙트")] // 인스펙터 제목을 달거에요 -> 효과 설정을
|
||||
[SerializeField] protected AudioClip hitSound, deathSound; // 변수를 선언할거에요 -> 효과음들을
|
||||
[SerializeField] protected GameObject deathEffectPrefab; // 변수를 선언할거에요 -> 사망 이펙트를
|
||||
[SerializeField] protected ParticleSystem hitEffect; // 변수를 선언할거에요 -> 피격 이펙트를
|
||||
[SerializeField] protected Transform impactSpawnPoint; // 변수를 선언할거에요 -> 이펙트 위치를
|
||||
|
||||
[Header("공통 사운드/이펙트")] // 인스펙터 창에 제목을 표시할거에요 -> 공통 사운드/이펙트 를
|
||||
[SerializeField] protected AudioClip hitSound, deathSound; // 변수를 선언할거에요 -> 피격음과 사망음 오디오 클립을
|
||||
[SerializeField] protected GameObject deathEffectPrefab; // 변수를 선언할거에요 -> 사망 시 재생할 이펙트 프리팹을
|
||||
[SerializeField] protected ParticleSystem hitEffect; // 변수를 선언할거에요 -> 피격 시 재생할 파티클 시스템을
|
||||
[SerializeField] protected Transform impactSpawnPoint; // 변수를 선언할거에요 -> 이펙트가 생성될 위치를
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// 생명주기
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
protected virtual void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 호출되는 Awake를
|
||||
protected virtual void Awake() // 함수를 실행할거에요 -> 초기화 Awake를
|
||||
{
|
||||
mobRenderer = GetComponentInChildren<Renderer>(); // 컴포넌트를 가져올거에요 -> 자식 오브젝트의 렌더러를 mobRenderer에
|
||||
animator = GetComponent<Animator>(); // 컴포넌트를 가져올거에요 -> 내 몸의 애니메이터를 animator에
|
||||
agent = GetComponent<NavMeshAgent>(); // 컴포넌트를 가져올거에요 -> 내 몸의 길찾기 에이전트를 agent에
|
||||
audioSource = GetComponent<AudioSource>(); // 컴포넌트를 가져올거에요 -> 내 몸의 오디오 소스를 audioSource에
|
||||
if (agent != null) agent.speed = moveSpeed; // 조건이 맞으면 설정할거에요 -> 에이전트가 있다면 이동 속도를 moveSpeed로
|
||||
mobRenderer = GetComponentInChildren<Renderer>(); // 가져올거에요 -> 렌더러를
|
||||
animator = GetComponent<Animator>(); // 가져올거에요 -> 애니메이터를
|
||||
agent = GetComponent<NavMeshAgent>(); // 가져올거에요 -> 에이전트를
|
||||
audioSource = GetComponent<AudioSource>(); // 가져올거에요 -> 오디오 소스를
|
||||
if (agent != null) agent.speed = moveSpeed; // 설정할거에요 -> 이동 속도를
|
||||
statusProcessor = new StatusEffectProcessor(this, this, agent, animator); // 생성할거에요 -> 상태이상 처리기를
|
||||
}
|
||||
|
||||
protected virtual void OnEnable() // 함수를 실행할거에요 -> 오브젝트가 활성화될 때 호출되는 OnEnable을
|
||||
protected virtual void OnEnable() // 함수를 실행할거에요 -> 활성화 시
|
||||
{
|
||||
playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform; // 오브젝트를 찾을거에요 -> "Player" 태그를 가진 오브젝트의 위치를 playerTransform에
|
||||
if (mobRenderer != null) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 있다면 보이게 켜기(true)를
|
||||
Init(); // 함수를 실행할거에요 -> 자식 클래스에서 정의할 초기화 함수 Init을
|
||||
if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.RegisterMob(this); // 조건이 맞으면 실행할거에요 -> 매니저가 있다면 나(this)를 관리 목록에 등록하기를
|
||||
playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform; // 찾을거에요 -> 플레이어를
|
||||
if (mobRenderer != null) mobRenderer.enabled = true; // 켤거에요 -> 렌더러를
|
||||
Init(); // 실행할거에요 -> 자식 초기화를
|
||||
if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.RegisterMob(this); // 등록할거에요 -> 매니저에
|
||||
}
|
||||
|
||||
protected virtual void OnDisable() // 함수를 실행할거에요 -> 오브젝트가 비활성화될 때 호출되는 OnDisable을
|
||||
protected virtual void OnDisable() // 함수를 실행할거에요 -> 비활성화 시
|
||||
{
|
||||
if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.UnregisterMob(this); // 조건이 맞으면 실행할거에요 -> 매니저가 있다면 나(this)를 관리 목록에서 제외하기를
|
||||
if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.UnregisterMob(this); // 해제할거에요 -> 매니저에서
|
||||
}
|
||||
|
||||
// ⭐ [수정] Update를 virtual로 선언하여 자식이 재정의 가능하게 함
|
||||
protected virtual void Update()
|
||||
protected virtual void Update() => OnManagedUpdate(); // 실행할거에요 -> 관리형 업데이트를
|
||||
|
||||
public void ResetStats() // 함수를 선언할거에요 -> 스탯 초기화를
|
||||
{
|
||||
OnManagedUpdate(); // 기본적으로 매니저 업데이트 로직을 수행 (일반 몬스터용)
|
||||
isDead = false; IsAggroed = false; currentHP = maxHP; // 초기화할거에요 -> 상태와 체력을
|
||||
OnHealthChanged?.Invoke(currentHP, maxHP); // 알릴거에요 -> 체력 변경을
|
||||
if (GetComponent<Collider>() != null) GetComponent<Collider>().enabled = true; // 켤거에요 -> 콜라이더를
|
||||
if (agent != null) agent.speed = moveSpeed; // 복구할거에요 -> 속도를
|
||||
OnResetStats(); // 실행할거에요 -> 추가 리셋을
|
||||
}
|
||||
|
||||
public void ResetStats() // 함수를 선언할거에요 -> 몬스터 상태를 초기화하는 ResetStats를
|
||||
// ⭐ [복구] 스포너에서 호출하는 재활성화 함수
|
||||
public virtual void Reactivate() // 함수를 선언할거에요 -> 재활성화를
|
||||
{
|
||||
isDead = false; // 상태를 바꿀거에요 -> 사망 상태를 거짓(false)으로
|
||||
IsAggroed = false; // 상태를 바꿀거에요 -> 어그로 상태를 거짓(false)으로
|
||||
currentHP = maxHP; // 값을 넣을거에요 -> 현재 체력을 최대 체력으로
|
||||
OnHealthChanged?.Invoke(currentHP, maxHP); // 이벤트를 알릴거에요 -> 체력이 변경되었음을 UI 등에
|
||||
|
||||
Collider col = GetComponent<Collider>(); // 컴포넌트를 가져올거에요 -> 내 몸의 콜라이더를 col에
|
||||
if (col != null) col.enabled = true; // 조건이 맞으면 실행할거에요 -> 콜라이더가 있다면 다시 켜기(true)를
|
||||
|
||||
if (agent != null) agent.speed = moveSpeed; // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 속도를 초기화하기를
|
||||
|
||||
OnResetStats(); // 함수를 실행할거에요 -> 자식 클래스의 추가 초기화 함수 OnResetStats를
|
||||
}
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// 추상 메서드 (자식이 반드시 구현)
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
protected virtual void Init() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 초기화 함수 Init을
|
||||
protected abstract void ExecuteAILogic(); // 추상 함수를 선언할거에요 -> 자식에서 반드시 구현해야 할 AI 로직 ExecuteAILogic을
|
||||
protected virtual void OnResetStats() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 스탯 초기화 함수 OnResetStats를
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// 공통 기능
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
public virtual void Reactivate() // 함수를 선언할거에요 -> 몬스터를 재활성화하는 Reactivate를
|
||||
{
|
||||
if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 있고 바닥에 있다면
|
||||
if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 정상이면
|
||||
{
|
||||
agent.isStopped = false; // 명령을 내릴거에요 -> 이동 멈춤을 풀라(false)고
|
||||
if (IsAggroed && playerTransform != null) // 조건이 맞으면 실행할거에요 -> 어그로가 끌렸고 플레이어가 있다면
|
||||
{
|
||||
agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로
|
||||
}
|
||||
agent.isStopped = false; // 명령을 내릴거에요 -> 이동 재개
|
||||
if (IsAggroed && playerTransform != null) agent.SetDestination(playerTransform.position); // 이동할거에요 -> 플레이어에게
|
||||
}
|
||||
if (mobRenderer != null) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 있다면 다시 켜기(true)를
|
||||
if (mobRenderer != null) mobRenderer.enabled = true; // 켤거에요 -> 렌더러를
|
||||
}
|
||||
|
||||
public virtual void OnManagedUpdate() // 함수를 선언할거에요 -> 매니저가 호출해줄 업데이트 함수 OnManagedUpdate를
|
||||
protected virtual void Init() { }
|
||||
protected abstract void ExecuteAILogic();
|
||||
protected virtual void OnResetStats() { }
|
||||
|
||||
public virtual void OnManagedUpdate() // 함수를 선언할거에요 -> 최적화 업데이트를
|
||||
{
|
||||
if (isDead || playerTransform == null || !gameObject.activeInHierarchy) return; // 조건이 맞으면 중단할거에요 -> 죽었거나, 플레이어가 없거나, 꺼져 있다면
|
||||
if (isDead || playerTransform == null || !gameObject.activeInHierarchy) return; // 중단할거에요 -> 작동 불가면
|
||||
|
||||
float distance = Vector3.Distance(transform.position, playerTransform.position); // 값을 계산할거에요 -> 나와 플레이어 사이의 거리를 distance에
|
||||
|
||||
if (distance > optimizationDistance && !IsAggroed) // 조건이 맞으면 실행할거에요 -> 거리가 멀고 어그로 상태가 아니라면
|
||||
float dist = Vector3.Distance(transform.position, playerTransform.position); // 계산할거에요 -> 거리를
|
||||
if (dist > optimizationDistance && !IsAggroed) // 조건이 맞으면 실행할거에요 -> 최적화 대상이면
|
||||
{
|
||||
StopMovement(); // 함수를 실행할거에요 -> 움직임을 멈추는 StopMovement를
|
||||
if (mobRenderer != null && mobRenderer.enabled) mobRenderer.enabled = false; // 조건이 맞으면 실행할거에요 -> 렌더러가 켜져 있다면 끄기(false)를 (최적화)
|
||||
return; // 중단할거에요 -> AI 로직을 실행하지 않도록 함수를
|
||||
StopMovement(); // 멈출거에요 -> 이동을
|
||||
if (mobRenderer != null) mobRenderer.enabled = false; // 끌거에요 -> 렌더러를
|
||||
return; // 중단할거에요 -> 로직을
|
||||
}
|
||||
|
||||
if (mobRenderer != null && !mobRenderer.enabled) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 꺼져 있다면 다시 켜기(true)를
|
||||
if (mobRenderer != null && !mobRenderer.isVisible) { StopMovement(); return; } // 조건이 맞으면 실행할거에요 -> 렌더러가 화면에 보이지 않는다면 멈추고 중단하기를
|
||||
if (agent != null && agent.isOnNavMesh && agent.isStopped) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 에이전트가 멈춰있다면 다시 움직이게(false) 하기를
|
||||
|
||||
ExecuteAILogic(); // 함수를 실행할거에요 -> 실제 몬스터의 AI 로직을
|
||||
if (mobRenderer != null) mobRenderer.enabled = true; // 켤거에요 -> 렌더러를
|
||||
if (agent != null && agent.isOnNavMesh && agent.isStopped) agent.isStopped = false; // 켤거에요 -> 이동을
|
||||
ExecuteAILogic(); // 실행할거에요 -> AI 로직을
|
||||
}
|
||||
|
||||
protected void StopMovement() // 함수를 선언할거에요 -> 이동을 멈추는 StopMovement를
|
||||
protected void StopMovement() // 함수를 선언할거에요 -> 정지 함수를
|
||||
{
|
||||
if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 정상 작동 중이라면
|
||||
{
|
||||
agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
|
||||
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 이동 속도를 0으로
|
||||
}
|
||||
if (animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면
|
||||
{
|
||||
animator.SetFloat("Speed", 0f); // 값을 전달할거에요 -> 속도 파라미터를 0으로
|
||||
animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션(Monster_Idle)을
|
||||
}
|
||||
if (agent != null && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 멈출거에요 -> 에이전트를
|
||||
if (animator != null) { animator.SetFloat("Speed", 0f); animator.Play(Monster_Idle); } // 재생할거에요 -> 대기 모션을
|
||||
}
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// 피격 / 사망
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
public virtual void TakeDamage(float amount) // 함수를 선언할거에요 -> 외부에서 데미지를 줄 때 호출할 TakeDamage를
|
||||
public virtual void TakeDamage(float amount) // 함수를 선언할거에요 -> 피격 처리를
|
||||
{
|
||||
OnDamaged(amount); // 함수를 실행할거에요 -> 실제 피격 처리를 담당하는 OnDamaged를
|
||||
if (isDead) return; // 중단할거에요 -> 죽었으면
|
||||
IsAggroed = true; currentHP -= amount; // 적용할거에요 -> 어그로와 데미지를
|
||||
OnHealthChanged?.Invoke(currentHP, maxHP); // 알릴거에요 -> UI에
|
||||
if (currentHP <= 0) Die(); else if (!isHit) StartHit(); // 분기할거에요 -> 사망 또는 피격으로
|
||||
}
|
||||
|
||||
public virtual void OnDamaged(float damage) // 함수를 선언할거에요 -> 데미지 계산 로직인 OnDamaged를
|
||||
protected virtual void StartHit() // 함수를 선언할거에요 -> 피격 시작을
|
||||
{
|
||||
if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면
|
||||
IsAggroed = true; // 상태를 바꿀거에요 -> 공격받았으니 어그로 상태를 참(true)으로
|
||||
currentHP -= damage; // 값을 뺄거에요 -> 체력에서 데미지(damage)만큼을
|
||||
OnHealthChanged?.Invoke(currentHP, maxHP); // 이벤트를 알릴거에요 -> 체력이 깎였음을
|
||||
if (currentHP <= 0) { Die(); return; } // 조건이 맞으면 실행할거에요 -> 체력이 0 이하라면 사망 함수(Die)를
|
||||
if (!isHit) StartHit(); // 조건이 맞으면 실행할거에요 -> 피격 상태가 아니라면 피격 연출(StartHit)을
|
||||
isHit = true; isAttacking = false; isResting = false; StopAllCoroutines(); // 초기화할거에요 -> 상태를
|
||||
OnStartHit(); // 실행할거에요 -> 추가 처리를
|
||||
if (agent && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 멈출거에요 -> 이동을
|
||||
animator.Play(Monster_GetDamage, 0, 0f); // 재생할거에요 -> 피격 애니를
|
||||
if (hitEffect) hitEffect.Play(); // 재생할거에요 -> 이펙트를
|
||||
if (hitSound) audioSource.PlayOneShot(hitSound); // 재생할거에요 -> 소리를
|
||||
}
|
||||
|
||||
protected virtual void StartHit() // 함수를 선언할거에요 -> 피격 연출을 시작하는 StartHit을
|
||||
protected virtual void OnStartHit() { if (myWeapon != null) myWeapon.DisableHitBox(); } // 가상 함수를 선언할거에요 -> 무기 끄기
|
||||
|
||||
public virtual void OnHitEnd() // 함수를 선언할거에요 -> 피격 종료를
|
||||
{
|
||||
isHit = true; // 상태를 바꿀거에요 -> 피격 중 상태를 참(true)으로
|
||||
isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로
|
||||
isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
|
||||
StopAllCoroutines(); // 중단할거에요 -> 실행 중인 모든 코루틴을
|
||||
|
||||
OnStartHit(); // 함수를 실행할거에요 -> 자식 클래스의 추가 피격 처리 OnStartHit을
|
||||
|
||||
if (agent && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 작동 중이라면
|
||||
{
|
||||
agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
|
||||
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
|
||||
}
|
||||
|
||||
animator.Play(Monster_GetDamage, 0, 0f); // 재생할거에요 -> 피격 애니메이션(Monster_GetDamage)을
|
||||
if (hitEffect) hitEffect.Play(); // 조건이 맞으면 실행할거에요 -> 피격 이펙트가 있다면 재생하기를
|
||||
if (hitSound) audioSource.PlayOneShot(hitSound); // 조건이 맞으면 실행할거에요 -> 피격 사운드가 있다면 재생하기를
|
||||
isHit = false; // 해제할거에요 -> 피격 상태를
|
||||
if (agent && agent.isOnNavMesh) agent.isStopped = false; // 재개할거에요 -> 이동을
|
||||
}
|
||||
|
||||
protected virtual void OnStartHit() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 피격 시작 함수 OnStartHit을
|
||||
|
||||
public virtual void OnHitEnd() // 함수를 선언할거에요 -> 피격 연출이 끝났을 때 호출할 OnHitEnd를
|
||||
protected virtual void Die() // 함수를 선언할거에요 -> 사망 처리를
|
||||
{
|
||||
isHit = false; // 상태를 바꿀거에요 -> 피격 중 상태를 거짓(false)으로
|
||||
if (agent && agent.isOnNavMesh) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 다시 움직이게(false) 하기를
|
||||
isDead = true; IsAggroed = false; OnDie(); // 설정할거에요 -> 사망 상태를
|
||||
OnMonsterKilled?.Invoke(expReward); // 알릴거에요 -> 경험치 지급을
|
||||
if (GetComponent<Collider>() != null) GetComponent<Collider>().enabled = false; // 끌거에요 -> 충돌을
|
||||
if (agent && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 멈출거에요 -> 이동을
|
||||
animator.Play(Monster_Die, 0, 0f); // 재생할거에요 -> 사망 애니를
|
||||
if (deathSound) audioSource.PlayOneShot(deathSound); // 재생할거에요 -> 사망 소리를
|
||||
Invoke(nameof(ReturnToPool), 1.5f); // 예약할거에요 -> 풀 반환을
|
||||
}
|
||||
|
||||
protected virtual void Die() // 함수를 선언할거에요 -> 몬스터 사망을 처리하는 Die를
|
||||
protected virtual void OnDie() { if (myWeapon != null) myWeapon.DisableHitBox(); } // 가상 함수를 선언할거에요 -> 사망 시 처리를
|
||||
protected void ReturnToPool() => gameObject.SetActive(false); // 함수를 선언할거에요 -> 비활성화를
|
||||
|
||||
public virtual void OnAttackStart() // 함수를 선언할거에요 -> 공격 시작을
|
||||
{
|
||||
if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽은 상태라면
|
||||
isDead = true; // 상태를 바꿀거에요 -> 사망 상태를 참(true)으로
|
||||
IsAggroed = false; // 상태를 바꿀거에요 -> 어그로 상태를 거짓(false)으로
|
||||
|
||||
OnDie(); // 함수를 실행할거에요 -> 자식 클래스의 추가 사망 처리 OnDie를
|
||||
|
||||
OnMonsterKilled?.Invoke(expReward); // 이벤트를 알릴거에요 -> 몬스터 처치와 경험치 보상(expReward)을
|
||||
Collider col = GetComponent<Collider>(); // 컴포넌트를 가져올거에요 -> 내 몸의 콜라이더를 col에
|
||||
if (col != null) col.enabled = false; // 조건이 맞으면 실행할거에요 -> 콜라이더가 있다면 끄기(false)를 (시체 밟기 방지)
|
||||
|
||||
if (agent && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 작동 중이라면
|
||||
{
|
||||
agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
|
||||
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
|
||||
}
|
||||
|
||||
animator.Play(Monster_Die, 0, 0f); // 재생할거에요 -> 사망 애니메이션(Monster_Die)을
|
||||
if (deathSound) audioSource.PlayOneShot(deathSound); // 조건이 맞으면 실행할거에요 -> 사망 사운드가 있다면 재생하기를
|
||||
Invoke("ReturnToPool", 1.5f); // 예약을 걸거에요 -> 1.5초 뒤에 풀로 반환하는 ReturnToPool 함수를
|
||||
isAttacking = true; isResting = false; // 설정할거에요 -> 공격 중으로
|
||||
if (myWeapon != null) myWeapon.EnableHitBox(); // 켤거에요 -> 무기 판정을
|
||||
}
|
||||
|
||||
protected virtual void OnDie() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 사망 처리 함수 OnDie를
|
||||
|
||||
public bool IsDead => isDead; // 프로퍼티를 선언할거에요 -> 외부에서 사망 여부를 확인할 수 있는 IsDead를
|
||||
protected void ReturnToPool() => gameObject.SetActive(false); // 함수를 선언할거에요 -> 오브젝트를 비활성화해서 풀로 돌려보내는 ReturnToPool을
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// ⭐ 공격 이벤트
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
// ⭐ [핵심 수정] 자식에서 재정의할 수 있도록 public virtual로 선언
|
||||
public virtual void OnAttackStart() // 함수를 선언할거에요 -> 공격 시작 시 호출되는 OnAttackStart를
|
||||
public virtual void OnAttackEnd() // 함수를 선언할거에요 -> 공격 종료를
|
||||
{
|
||||
isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로
|
||||
isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
|
||||
|
||||
if (myWeapon != null) // 조건이 맞으면 실행할거에요 -> 무기가 있다면
|
||||
{
|
||||
myWeapon.EnableHitBox(); // 함수를 실행할거에요 -> 무기의 공격 판정을 켜는 EnableHitBox를
|
||||
}
|
||||
if (myWeapon != null) myWeapon.DisableHitBox(); // 끌거에요 -> 무기 판정을
|
||||
isAttacking = false; // 해제할거에요 -> 공격 상태를
|
||||
if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 시작할거에요 -> 휴식을
|
||||
}
|
||||
|
||||
// ⭐ [핵심 수정] 자식에서 재정의할 수 있도록 public virtual로 선언
|
||||
public virtual void OnAttackEnd() // 함수를 선언할거에요 -> 공격 종료 시 호출되는 OnAttackEnd를
|
||||
protected virtual IEnumerator RestAfterAttack() // 코루틴을 선언할거에요 -> 휴식 로직을
|
||||
{
|
||||
if (myWeapon != null) // 조건이 맞으면 실행할거에요 -> 무기가 있다면
|
||||
{
|
||||
myWeapon.DisableHitBox(); // 함수를 실행할거에요 -> 무기의 공격 판정을 끄는 DisableHitBox를
|
||||
}
|
||||
|
||||
isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로
|
||||
if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 조건이 맞으면 실행할거에요 -> 살아있고 안 맞았다면 휴식 코루틴(RestAfterAttack)을
|
||||
isResting = true; // 설정할거에요 -> 휴식 중으로
|
||||
yield return new WaitForSeconds(attackRestDuration); // 기다릴거에요 -> 휴식 시간만큼
|
||||
isResting = false; // 해제할거에요 -> 휴식 상태를
|
||||
}
|
||||
|
||||
protected virtual IEnumerator RestAfterAttack() // 코루틴 함수를 선언할거에요 -> 공격 후 잠시 쉬는 RestAfterAttack을
|
||||
public void ApplyStatusEffect(StatusEffectType type, float dmg, float dur) // 함수를 선언할거에요 -> 상태이상 적용을
|
||||
{
|
||||
isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참(true)으로
|
||||
yield return new WaitForSeconds(attackRestDuration); // 기다릴거에요 -> 휴식 시간(attackRestDuration)만큼
|
||||
isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
|
||||
}
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// ⭐ 상태 이상 시스템
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
public void ApplyStatusEffect(StatusEffectType type, float damage, float duration) // 함수를 선언할거에요 -> 상태이상을 적용하는 ApplyStatusEffect를
|
||||
{
|
||||
if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면
|
||||
|
||||
switch (type) // 분기할거에요 -> 상태이상 종류(type)에 따라
|
||||
{
|
||||
case StatusEffectType.Burn: // 조건이 맞으면 실행할거에요 -> 화상(Burn)이라면
|
||||
StartCoroutine(BurnCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 화상 효과를 주는 BurnCoroutine을
|
||||
break;
|
||||
case StatusEffectType.Slow: // 조건이 맞으면 실행할거에요 -> 슬로우(Slow)라면
|
||||
StartCoroutine(SlowCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 느리게 만드는 SlowCoroutine을
|
||||
break;
|
||||
case StatusEffectType.Poison: // 조건이 맞으면 실행할거에요 -> 독(Poison)이라면
|
||||
StartCoroutine(PoisonCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 독 효과를 주는 PoisonCoroutine을
|
||||
break;
|
||||
case StatusEffectType.Shock: // 조건이 맞으면 실행할거에요 -> 충격(Shock)이라면
|
||||
TakeDamage(damage); // 함수를 실행할거에요 -> 충격 데미지를 즉시 적용하기를
|
||||
StartCoroutine(StunCoroutine(0.5f)); // 코루틴을 실행할거에요 -> 0.5초간 기절시키는 StunCoroutine을
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator BurnCoroutine(float tickDamage, float duration) // 코루틴 함수를 선언할거에요 -> 화상 효과를 처리할 BurnCoroutine을
|
||||
{
|
||||
float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로
|
||||
float tickInterval = 0.5f; // 변수를 초기화할거에요 -> 데미지 간격을 0.5초로
|
||||
while (elapsed < duration) // 반복할거에요 -> 경과 시간이 지속 시간보다 작을 동안
|
||||
{
|
||||
TakeDamage(tickDamage * tickInterval); // 함수를 실행할거에요 -> 틱 데미지를 입히는 TakeDamage를
|
||||
yield return new WaitForSeconds(tickInterval); // 기다릴거에요 -> 0.5초의 시간을
|
||||
elapsed += tickInterval; // 값을 더할거에요 -> 경과 시간에 0.5초를
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator SlowCoroutine(float amount, float duration) // 코루틴 함수를 선언할거에요 -> 슬로우 효과를 처리할 SlowCoroutine을
|
||||
{
|
||||
if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
|
||||
{
|
||||
float originalSpeed = agent.speed; // 값을 저장할거에요 -> 원래 속도를 originalSpeed에
|
||||
agent.speed *= (1f - Mathf.Clamp01(amount / 100f)); // 값을 변경할거에요 -> 현재 속도를 비율(amount)만큼 줄여서
|
||||
yield return new WaitForSeconds(duration); // 기다릴거에요 -> 지속 시간(duration)만큼
|
||||
agent.speed = originalSpeed; // 값을 복구할거에요 -> 속도를 원래대로
|
||||
}
|
||||
else // 조건이 틀리면 실행할거에요 -> 에이전트가 없다면
|
||||
{
|
||||
yield return new WaitForSeconds(duration); // 기다릴거에요 -> 시간만 때우기를
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator PoisonCoroutine(float tickDamage, float duration) // 코루틴 함수를 선언할거에요 -> 독 효과를 처리할 PoisonCoroutine을
|
||||
{
|
||||
float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로
|
||||
float tickInterval = 1f; // 변수를 초기화할거에요 -> 데미지 간격을 1초로
|
||||
while (elapsed < duration) // 반복할거에요 -> 경과 시간이 지속 시간보다 작을 동안
|
||||
{
|
||||
TakeDamage(tickDamage * tickInterval); // 함수를 실행할거에요 -> 틱 데미지를 입히는 TakeDamage를
|
||||
yield return new WaitForSeconds(tickInterval); // 기다릴거에요 -> 1초의 시간을
|
||||
elapsed += tickInterval; // 값을 더할거에요 -> 경과 시간에 1초를
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator StunCoroutine(float duration) // 코루틴 함수를 선언할거에요 -> 기절 효과를 처리할 StunCoroutine을
|
||||
{
|
||||
if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
|
||||
{
|
||||
agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
|
||||
if (animator != null) animator.speed = 0; // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 재생 속도를 0으로 (얼음 땡)
|
||||
|
||||
yield return new WaitForSeconds(duration); // 기다릴거에요 -> 지속 시간(duration)만큼
|
||||
|
||||
if (!isDead) // 조건이 맞으면 실행할거에요 -> 아직 살아있다면
|
||||
{
|
||||
agent.isStopped = false; // 명령을 내릴거에요 -> 이동 멈춤을 풀라(false)고
|
||||
if (animator != null) animator.speed = 1; // 조건이 맞으면 실행할거에요 -> 애니메이션 속도를 정상(1)으로
|
||||
}
|
||||
}
|
||||
else // 조건이 틀리면 실행할거에요 -> 에이전트가 없다면
|
||||
{
|
||||
yield return new WaitForSeconds(duration); // 기다릴거에요 -> 시간만 때우기를
|
||||
if (isDead) return; // 중단할거에요 -> 죽었으면
|
||||
switch (type)
|
||||
{ // 분기할거에요 -> 타입에 따라
|
||||
case StatusEffectType.Burn: statusProcessor.ApplyBurn(dmg, dur); break; // 적용할거에요 -> 화상을
|
||||
case StatusEffectType.Slow: statusProcessor.ApplySlow(dmg, dur); break; // 적용할거에요 -> 슬로우를
|
||||
case StatusEffectType.Poison: statusProcessor.ApplyBurn(dmg, dur); break; // 적용할거에요 -> 독을
|
||||
case StatusEffectType.Shock: statusProcessor.ApplyShock(dmg, dur); break; // 적용할거에요 -> 충격을
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,182 +1,69 @@
|
|||
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||
using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
|
||||
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
||||
using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
|
||||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
using System.Collections.Generic; // 리스트를 사용할거에요 -> 제네릭을
|
||||
|
||||
public class MeleeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 MeleeMonster를
|
||||
public class MeleeMonster : MonsterClass // 클래스를 선언할거에요 -> 근접 몬스터를
|
||||
{
|
||||
[Header("=== 근접 공격 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 근접 공격 설정 === 을
|
||||
[SerializeField] private float attackRange = 2f; // 변수를 선언할거에요 -> 공격 사거리(2.0)를 attackRange에
|
||||
[SerializeField] private float attackDelay = 1.5f; // 변수를 선언할거에요 -> 공격 딜레이(1.5초)를 attackDelay에
|
||||
[Header("=== 공격 설정 ===")] // 인스펙터 제목을 달거에요 -> 공격 설정을
|
||||
[SerializeField] private float attackRange = 2f; // 변수를 선언할거에요 -> 사거리를
|
||||
[SerializeField] private float attackDelay = 1.5f; // 변수를 선언할거에요 -> 딜레이를
|
||||
|
||||
[Header("=== 드랍 아이템 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 드랍 아이템 설정 === 을
|
||||
[Tooltip("드랍 가능한 아이템들을 여기에 여러 개 등록하세요. (랜덤 1개 드랍)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
[SerializeField] private List<GameObject> dropItemPrefabs; // 리스트를 선언할거에요 -> 드랍할 아이템 프리팹 목록을 dropItemPrefabs에
|
||||
[Header("=== 드랍 ===")] // 인스펙터 제목을 달거에요 -> 드랍 설정을
|
||||
[SerializeField] private List<GameObject> dropItemPrefabs; // 변수를 선언할거에요 -> 아이템 목록을
|
||||
[Range(0, 100)][SerializeField] private float dropChance = 30f; // 변수를 선언할거에요 -> 확률을
|
||||
|
||||
[Tooltip("아이템이 나올 확률 (0 ~ 100%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
[Range(0, 100)][SerializeField] private float dropChance = 30f; // 변수를 선언할거에요 -> 드랍 확률(30%)을 dropChance에
|
||||
[Header("애니메이션")] // 인스펙터 제목을 달거에요 -> 애니메이션을
|
||||
[SerializeField] private string[] attackAnimations = { "Monster_Attack_1" }; // 변수를 선언할거에요 -> 공격 애니들을
|
||||
[SerializeField] private string Monster_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니를
|
||||
|
||||
private float lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간을 lastAttackTime에
|
||||
private float lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간을
|
||||
private int attackIndex; // 변수를 선언할거에요 -> 공격 인덱스를
|
||||
|
||||
[Header("공격 / 이동 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 공격 / 이동 애니메이션 을
|
||||
[SerializeField] private string[] attackAnimations = { "Monster_Attack_1" }; // 배열을 선언할거에요 -> 공격 애니메이션 이름들을 attackAnimations에
|
||||
[SerializeField] private string Monster_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 Monster_Walk에
|
||||
|
||||
[Header("AI 상세 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 상세 설정 을
|
||||
[SerializeField] private float stopBuffer = 0.3f; // 변수를 선언할거에요 -> 정지 여유 거리(0.3)를 stopBuffer에
|
||||
[SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에
|
||||
[SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 대기 시간(2.0)을 patrolInterval에
|
||||
|
||||
private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에
|
||||
private float repathInterval = 0.3f; // 변수를 선언할거에요 -> 경로 갱신 주기(0.3초)를 repathInterval에
|
||||
private float nextRepathTime; // 변수를 선언할거에요 -> 다음 경로 갱신 시간을 nextRepathTime에
|
||||
private int attackIndex; // 변수를 선언할거에요 -> 현재 공격 애니메이션 인덱스를 attackIndex에
|
||||
private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어가 감지 구역에 있는지 여부를 isPlayerInZone에
|
||||
|
||||
protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을
|
||||
protected override void Init() // 함수를 실행할거에요 -> 초기화를
|
||||
{
|
||||
if (agent != null) agent.stoppingDistance = attackRange - 0.4f; // 조건이 맞으면 설정할거에요 -> 에이전트 정지 거리를 사거리보다 약간 짧게
|
||||
if (animator != null) animator.applyRootMotion = false; // 조건이 맞으면 설정할거에요 -> 애니메이션 이동(Root Motion)을 끄기로
|
||||
if (agent != null) agent.stoppingDistance = attackRange - 0.4f; // 설정할거에요 -> 정지 거리를
|
||||
if (animator != null) animator.applyRootMotion = false; // 끌거에요 -> 루트 모션을
|
||||
}
|
||||
|
||||
protected override void OnResetStats() // 함수를 덮어씌워 실행할거에요 -> 스탯 초기화 로직인 OnResetStats를
|
||||
protected override void OnResetStats() => myWeapon?.SetDamage(attackDamage); // 함수를 실행할거에요 -> 무기 데미지 설정을
|
||||
|
||||
protected override void ExecuteAILogic() // 함수를 실행할거에요 -> AI 로직을
|
||||
{
|
||||
if (myWeapon != null) // 조건이 맞으면 실행할거에요 -> 무기가 있다면
|
||||
if (isHit || isAttacking || isResting) return; // 중단할거에요 -> 행동 불가면
|
||||
|
||||
float dist = Vector3.Distance(transform.position, playerTransform.position); // 계산할거에요 -> 거리를
|
||||
if (dist <= attackRange) TryAttack(); // 분기할거에요 -> 가까우면 공격을
|
||||
else MoveToPlayer(); // 분기할거에요 -> 멀면 이동을
|
||||
}
|
||||
|
||||
private void MoveToPlayer() // 함수를 선언할거에요 -> 이동 로직을
|
||||
{
|
||||
if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(playerTransform.position); } // 이동할거에요 -> 플레이어에게
|
||||
if (agent.velocity.magnitude > 0.1f) animator.Play(Monster_Walk); // 재생할거에요 -> 걷기 애니를
|
||||
else animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니를
|
||||
}
|
||||
|
||||
private void TryAttack() // 함수를 선언할거에요 -> 공격 시도를
|
||||
{
|
||||
if (Time.time < lastAttackTime + attackDelay) return; // 중단할거에요 -> 쿨타임이면
|
||||
lastAttackTime = Time.time; // 갱신할거에요 -> 공격 시간을
|
||||
isAttacking = true; // 설정할거에요 -> 공격 중으로
|
||||
if (agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 멈출거에요 -> 이동을
|
||||
|
||||
string animName = attackAnimations[attackIndex]; // 가져올거에요 -> 애니 이름을
|
||||
attackIndex = (attackIndex + 1) % attackAnimations.Length; // 갱신할거에요 -> 인덱스를
|
||||
animator.Play(animName, 0, 0f); // 재생할거에요 -> 공격 애니를
|
||||
}
|
||||
|
||||
protected override void OnDie() // 함수를 실행할거에요 -> 사망 시
|
||||
{
|
||||
base.OnDie(); // 실행할거에요 -> 부모 사망 로직을
|
||||
if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) // 조건이 맞으면 실행할거에요 -> 아이템이 있으면
|
||||
{
|
||||
myWeapon.SetDamage(attackDamage); // 함수를 실행할거에요 -> 무기 공격력을 몬스터 공격력으로 설정을
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을
|
||||
{
|
||||
if (isHit || isAttacking || isResting) return; // 조건이 맞으면 중단할거에요 -> 피격, 공격, 휴식 중이라면
|
||||
|
||||
float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 나와 플레이어 사이의 거리를 distance에
|
||||
|
||||
if (isPlayerInZone || distance <= attackRange * 2f) // 조건이 맞으면 실행할거에요 -> 감지 구역 안이거나 거리가 가까우면
|
||||
HandlePlayerTarget(); // 함수를 실행할거에요 -> 추격 및 공격 처리를 하는 HandlePlayerTarget을
|
||||
else // 조건이 틀리면 실행할거에요 -> 멀리 있다면
|
||||
Patrol(); // 함수를 실행할거에요 -> 주변을 배회하는 Patrol을
|
||||
|
||||
UpdateMovementAnimation(); // 함수를 실행할거에요 -> 애니메이션 상태를 갱신하는 UpdateMovementAnimation을
|
||||
}
|
||||
|
||||
void HandlePlayerTarget() // 함수를 선언할거에요 -> 타겟 처리 로직인 HandlePlayerTarget을
|
||||
{
|
||||
float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를
|
||||
|
||||
if (distance <= attackRange - stopBuffer) // 조건이 맞으면 실행할거에요 -> 사거리 안쪽으로 충분히 들어왔다면
|
||||
TryAttack(); // 함수를 실행할거에요 -> 공격을 시도하는 TryAttack을
|
||||
else if (Time.time >= nextRepathTime) // 조건이 맞으면 실행할거에요 -> 경로 갱신 시간이 되었다면
|
||||
{
|
||||
if (agent.isOnNavMesh) agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로
|
||||
nextRepathTime = Time.time + repathInterval; // 값을 갱신할거에요 -> 다음 경로 갱신 시간을
|
||||
}
|
||||
}
|
||||
|
||||
void TryAttack() // 함수를 선언할거에요 -> 공격을 수행하는 TryAttack을
|
||||
{
|
||||
if (Time.time < lastAttackTime + attackDelay) return; // 조건이 맞으면 중단할거에요 -> 아직 쿨타임이 안 지났다면
|
||||
lastAttackTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 공격 시간으로
|
||||
|
||||
string attackName = attackAnimations[attackIndex]; // 값을 가져올거에요 -> 현재 인덱스의 공격 애니메이션 이름을
|
||||
attackIndex = (attackIndex + 1) % attackAnimations.Length; // 값을 갱신할거에요 -> 다음 공격 인덱스로 (순환)
|
||||
|
||||
isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로
|
||||
|
||||
if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 바닥에 있다면
|
||||
{
|
||||
agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고
|
||||
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
|
||||
}
|
||||
|
||||
animator.Play(attackName, 0, 0f); // 재생할거에요 -> 공격 애니메이션을 처음부터
|
||||
}
|
||||
|
||||
void UpdateMovementAnimation() // 함수를 선언할거에요 -> 이동 애니메이션을 제어하는 UpdateMovementAnimation을
|
||||
{
|
||||
if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면
|
||||
|
||||
if (agent.velocity.magnitude < 0.1f) // 조건이 맞으면 실행할거에요 -> 거의 멈춰 있다면
|
||||
animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션을
|
||||
else // 조건이 틀리면 실행할거에요 -> 움직이고 있다면
|
||||
animator.Play(Monster_Walk); // 재생할거에요 -> 걷기 애니메이션을
|
||||
}
|
||||
|
||||
void Patrol() // 함수를 선언할거에요 -> 순찰 로직인 Patrol을
|
||||
{
|
||||
if (Time.time < nextPatrolTime) return; // 조건이 맞으면 중단할거에요 -> 아직 대기 시간이라면
|
||||
|
||||
Vector3 randomPoint = transform.position + new Vector3( // 벡터를 계산할거에요 -> 현재 위치 주변의 랜덤한 지점을
|
||||
Random.Range(-patrolRadius, patrolRadius),
|
||||
0,
|
||||
Random.Range(-patrolRadius, patrolRadius)
|
||||
);
|
||||
|
||||
if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> 랜덤 지점이 NavMesh 위라면
|
||||
if (agent.isOnNavMesh) agent.SetDestination(hit.position); // 명령을 내릴거에요 -> 그 지점으로 이동하라고
|
||||
|
||||
nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을
|
||||
}
|
||||
|
||||
public override void OnAttackStart() // 함수를 덮어씌워 실행할거에요 -> 공격 시작 시 호출되는 OnAttackStart를
|
||||
{
|
||||
isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로
|
||||
isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
|
||||
if (myWeapon != null) myWeapon.EnableHitBox(); // 조건이 맞으면 실행할거에요 -> 무기 공격 판정을 켜기를
|
||||
}
|
||||
|
||||
public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 시 호출되는 OnAttackEnd를
|
||||
{
|
||||
if (myWeapon != null) myWeapon.DisableHitBox(); // 조건이 맞으면 실행할거에요 -> 무기 공격 판정을 끄기를
|
||||
isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로
|
||||
if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 조건이 맞으면 실행할거에요 -> 살아있고 안 맞았다면 휴식 코루틴을
|
||||
}
|
||||
|
||||
protected override IEnumerator RestAfterAttack() // 코루틴 함수를 덮어씌워 실행할거에요 -> 공격 후 휴식하는 RestAfterAttack을
|
||||
{
|
||||
isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참(true)으로
|
||||
yield return new WaitForSeconds(attackRestDuration); // 기다릴거에요 -> 휴식 시간만큼
|
||||
isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
|
||||
}
|
||||
|
||||
protected override void OnStartHit() // 함수를 덮어씌워 실행할거에요 -> 피격 시작 시 호출되는 OnStartHit을
|
||||
{
|
||||
if (myWeapon != null) myWeapon.DisableHitBox(); // 조건이 맞으면 실행할거에요 -> 피격 당했으니 공격 판정을 끄기를
|
||||
}
|
||||
|
||||
// 🎲 [핵심 수정] 리스트에서 랜덤 뽑기 + 확률 체크
|
||||
protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 사망 시 호출되는 OnDie를
|
||||
{
|
||||
if (myWeapon != null) myWeapon.DisableHitBox(); // 조건이 맞으면 실행할거에요 -> 공격 판정을 끄기를
|
||||
|
||||
// 1. 리스트에 아이템이 하나라도 있는지 확인
|
||||
if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) // 조건이 맞으면 실행할거에요 -> 드랍 테이블이 비어있지 않다면
|
||||
{
|
||||
// 2. 확률 체크 (0 ~ 100)
|
||||
float randomValue = Random.Range(0f, 100f); // 값을 뽑을거에요 -> 0부터 100 사이의 랜덤값을
|
||||
|
||||
if (randomValue <= dropChance) // 당첨! // 조건이 맞으면 실행할거에요 -> 랜덤값이 드랍 확률 이하라면
|
||||
if (Random.Range(0f, 100f) <= dropChance) // 조건이 맞으면 실행할거에요 -> 확률 당첨되면
|
||||
{
|
||||
// 3. 리스트에서 랜덤하게 하나 뽑기 (0번 ~ 마지막 번호 중 하나)
|
||||
int randomIndex = Random.Range(0, dropItemPrefabs.Count); // 값을 뽑을거에요 -> 아이템 리스트 인덱스를 랜덤으로
|
||||
GameObject selectedItem = dropItemPrefabs[randomIndex]; // 오브젝트를 가져올거에요 -> 선택된 아이템 프리팹을
|
||||
|
||||
if (selectedItem != null) // 조건이 맞으면 실행할거에요 -> 아이템이 유효하다면
|
||||
{
|
||||
Instantiate(selectedItem, transform.position + Vector3.up * 0.5f, Quaternion.identity); // 생성할거에요 -> 아이템을 몬스터 위치에
|
||||
}
|
||||
int idx = Random.Range(0, dropItemPrefabs.Count); // 뽑을거에요 -> 랜덤 인덱스를
|
||||
if (dropItemPrefabs[idx] != null) Instantiate(dropItemPrefabs[idx], transform.position + Vector3.up * 0.5f, Quaternion.identity); // 생성할거에요 -> 아이템을
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 트리거에 들어왔을 때 OnTriggerEnter를
|
||||
{
|
||||
if (other.CompareTag("Player")) isPlayerInZone = true; // 조건이 맞으면 실행할거에요 -> 플레이어라면 감지 구역 진입 상태를 참(true)으로
|
||||
}
|
||||
|
||||
private void OnTriggerExit(Collider other) // 함수를 실행할거에요 -> 트리거에서 나갔을 때 OnTriggerExit을
|
||||
{
|
||||
if (other.CompareTag("Player")) isPlayerInZone = false; // 조건이 맞으면 실행할거에요 -> 플레이어라면 감지 구역 진입 상태를 거짓(false)으로
|
||||
}
|
||||
}
|
||||
|
|
@ -1,208 +1,75 @@
|
|||
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||
using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
|
||||
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
||||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
|
||||
/// <summary>
|
||||
/// 통합 원거리 몬스터 (키 작은 플레이어 머리 조준 기능 추가)
|
||||
/// 파일 이름을 반드시 [ UniversalRangedMonster.cs ] 로 맞춰주세요!
|
||||
/// </summary>
|
||||
public class UniversalRangedMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 UniversalRangedMonster를
|
||||
public class UniversalRangedMonster : MonsterClass // 클래스를 선언할거에요 -> 원거리 몬스터를
|
||||
{
|
||||
public enum AttackStyle { Straight, Lob } // 열거형을 정의할거에요 -> 공격 방식(직선, 곡사)을 정의하는 AttackStyle을
|
||||
public enum AttackStyle { Straight, Lob } // 열거형을 정의할거에요 -> 공격 타입을
|
||||
[Header("=== 원거리 설정 ===")] // 인스펙터 제목을 달거에요 -> 원거리 설정을
|
||||
[SerializeField] private AttackStyle attackStyle = AttackStyle.Straight; // 변수를 선언할거에요 -> 스타일을
|
||||
[SerializeField] private GameObject projectilePrefab; // 변수를 선언할거에요 -> 투사체를
|
||||
[SerializeField] private Transform firePoint; // 변수를 선언할거에요 -> 발사 위치를
|
||||
[SerializeField] private float attackRange = 10f; // 변수를 선언할거에요 -> 사거리를
|
||||
[SerializeField] private float attackDelay = 2f; // 변수를 선언할거에요 -> 딜레이를
|
||||
[SerializeField] private float projectileSpeed = 20f; // 변수를 선언할거에요 -> 속도를
|
||||
|
||||
[Header("=== 🏹 공격 스타일 선택 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 🏹 공격 스타일 선택 === 을
|
||||
[SerializeField] private AttackStyle attackStyle = AttackStyle.Straight; // 변수를 선언할거에요 -> 공격 스타일(기본 직선)을 attackStyle에
|
||||
[Header("곡사 설정")] // 인스펙터 제목을 달거에요 -> 곡사 설정을
|
||||
[SerializeField] private float launchAngle = 45f; // 변수를 선언할거에요 -> 각도를
|
||||
[SerializeField] private float aimHeightOffset = 1.2f; // 변수를 선언할거에요 -> 높이 보정을
|
||||
|
||||
[Header("공통 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 공통 설정 을
|
||||
[SerializeField] private GameObject projectilePrefab; // 변수를 선언할거에요 -> 발사체 프리팹을 projectilePrefab에
|
||||
[SerializeField] private Transform firePoint; // 변수를 선언할거에요 -> 발사 위치를 firePoint에
|
||||
[SerializeField] private float attackRange = 10f; // 변수를 선언할거에요 -> 공격 사거리(10.0)를 attackRange에
|
||||
[SerializeField] private float attackDelay = 2f; // 변수를 선언할거에요 -> 공격 딜레이(2.0초)를 attackDelay에
|
||||
[SerializeField] private float detectRange = 15f; // 변수를 선언할거에요 -> 인식 거리(15.0)를 detectRange에
|
||||
[Header("애니메이션")] // 인스펙터 제목을 달거에요 -> 애니메이션을
|
||||
[SerializeField] private string attackAnim = "Monster_Attack"; // 변수를 선언할거에요 -> 공격 애니를
|
||||
[SerializeField] private string walkAnim = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니를
|
||||
|
||||
[Header("🔹 직선 발사 설정 (활/총)")] // 인스펙터 창에 제목을 표시할거에요 -> 🔹 직선 발사 설정 (활/총) 을
|
||||
[SerializeField] private float projectileSpeed = 20f; // 변수를 선언할거에요 -> 투사체 속도(20.0)를 projectileSpeed에
|
||||
[SerializeField] private float minDistance = 5f; // 변수를 선언할거에요 -> 최소 거리(도망가는 거리)를 minDistance에
|
||||
private float lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간을
|
||||
|
||||
[Header("🔸 곡사 투척 설정 (돌/폭탄)")] // 인스펙터 창에 제목을 표시할거에요 -> 🔸 곡사 투척 설정 (돌/폭탄) 을
|
||||
[Tooltip("체크하면 거리/높이를 계산해서 정확히 맞춥니다.")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
[SerializeField] private bool usePreciseLob = true; // 변수를 선언할거에요 -> 정밀 곡사 사용 여부를 usePreciseLob에
|
||||
protected override void Init() { if (agent != null) agent.stoppingDistance = attackRange * 0.8f; } // 초기화할거에요 -> 정지 거리를
|
||||
|
||||
[Tooltip("던지는 각도 (45도가 최대 사거리)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
[Range(15f, 75f)][SerializeField] private float launchAngle = 45f; // 변수를 선언할거에요 -> 발사 각도(45도)를 launchAngle에
|
||||
|
||||
// ⭐ [추가됨] 조준 높이 보정 (키 작은 플레이어 해결용)
|
||||
[Tooltip("0이면 발가락, 1.0이면 명치, 1.5면 머리를 노립니다.")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
[SerializeField] private float aimHeight = 1.2f; // 변수를 선언할거에요 -> 조준 높이 오프셋(1.2)을 aimHeight에
|
||||
|
||||
[Header("🏃♂️ 도망 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 🏃♂️ 도망 설정 을
|
||||
[SerializeField] private float fleeDistance = 5f; // 변수를 선언할거에요 -> 도망가는 거리(5.0)를 fleeDistance에
|
||||
|
||||
[Header("애니메이션 & 기타")] // 인스펙터 창에 제목을 표시할거에요 -> 애니메이션 & 기타 를
|
||||
[SerializeField] private float throwForce = 15f; // 변수를 선언할거에요 -> 기본 투척 힘을 throwForce에
|
||||
[SerializeField] private float throwUpward = 5f; // 변수를 선언할거에요 -> 기본 상향 힘을 throwUpward에
|
||||
[SerializeField] private float reloadTime = 2.0f; // 변수를 선언할거에요 -> 장전 시간(2.0초)을 reloadTime에
|
||||
[SerializeField] private GameObject handModel; // 변수를 선언할거에요 -> 손에 든 무기 모델을 handModel에
|
||||
[SerializeField] private bool aimAtPlayer = true; // 변수를 선언할거에요 -> 플레이어 조준 여부를 aimAtPlayer에
|
||||
|
||||
[SerializeField] private string attackAnim = "Monster_Attack"; // 변수를 선언할거에요 -> 공격 애니메이션 이름을 attackAnim에
|
||||
[SerializeField] private string walkAnim = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 walkAnim에
|
||||
[SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에
|
||||
[SerializeField] private float patrolInterval = 3f; // 변수를 선언할거에요 -> 순찰 간격(3.0초)을 patrolInterval에
|
||||
|
||||
private float lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간을 lastAttackTime에
|
||||
private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에
|
||||
private bool isReloading = false; // 변수를 선언할거에요 -> 장전 중 여부를 isReloading에
|
||||
|
||||
protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을
|
||||
protected override void ExecuteAILogic() // 함수를 실행할거에요 -> AI 로직을
|
||||
{
|
||||
if (agent != null) { agent.stoppingDistance = attackRange * 0.8f; agent.speed = 3.5f; } // 조건이 맞으면 설정할거에요 -> 정지 거리를 사거리의 80%로, 속도를 3.5로
|
||||
if (animator != null) animator.applyRootMotion = false; // 조건이 맞으면 설정할거에요 -> 애니메이션 이동을 끄기로
|
||||
if (isHit || isAttacking || isResting) return; // 중단할거에요 -> 행동 불가면
|
||||
float dist = Vector3.Distance(transform.position, playerTransform.position); // 계산할거에요 -> 거리를
|
||||
if (dist <= attackRange) TryAttack(); else ChasePlayer(); // 분기할거에요 -> 공격 또는 추격으로
|
||||
}
|
||||
|
||||
protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을
|
||||
private void ChasePlayer() // 함수를 선언할거에요 -> 추격 로직을
|
||||
{
|
||||
if (isHit || isAttacking || isResting || isReloading) return; // 조건이 맞으면 중단할거에요 -> 피격, 공격, 휴식, 장전 중이라면
|
||||
if (isAttacking && animator.GetCurrentAnimatorStateInfo(0).IsName(Monster_Idle)) OnAttackEnd(); // 조건이 맞으면 실행할거에요 -> 공격 중인데 모션이 대기라면 강제로 공격 종료를
|
||||
if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(playerTransform.position); } // 이동할거에요 -> 플레이어에게
|
||||
animator.Play(walkAnim); // 재생할거에요 -> 걷기 애니를
|
||||
}
|
||||
|
||||
float dist = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를
|
||||
private void TryAttack() // 함수를 선언할거에요 -> 공격 시도를
|
||||
{
|
||||
if (Time.time < lastAttackTime + attackDelay) return; // 중단할거에요 -> 쿨타임 중이면
|
||||
lastAttackTime = Time.time; isAttacking = true; // 갱신할거에요 -> 시간과 상태를
|
||||
if (agent.isOnNavMesh) agent.isStopped = true; // 멈출거에요 -> 이동을
|
||||
|
||||
if (dist <= detectRange) HandleCombat(dist); // 조건이 맞으면 실행할거에요 -> 감지 거리 이내라면 전투 처리(HandleCombat)를
|
||||
else // 조건이 틀리면 실행할거에요 -> 멀리 있다면
|
||||
Vector3 dir = (playerTransform.position - transform.position).normalized; dir.y = 0; // 계산할거에요 -> 방향을
|
||||
transform.rotation = Quaternion.LookRotation(dir); // 회전할거에요 -> 방향대로
|
||||
animator.Play(attackAnim); // 재생할거에요 -> 공격 애니를
|
||||
}
|
||||
|
||||
public override void OnAttackStart() // 함수를 실행할거에요 -> 공격 이벤트 발생 시
|
||||
{
|
||||
if (projectilePrefab == null || firePoint == null) return; // 중단할거에요 -> 설정 없으면
|
||||
GameObject obj = Instantiate(projectilePrefab, firePoint.position, transform.rotation); // 생성할거에요 -> 투사체를
|
||||
Projectile proj = obj.GetComponent<Projectile>(); // 가져올거에요 -> 스크립트를
|
||||
|
||||
if (attackStyle == AttackStyle.Straight) // 조건이 맞으면 실행할거에요 -> 직선형이면
|
||||
{
|
||||
if (agent.hasPath && agent.destination == playerTransform.position) { agent.ResetPath(); nextPatrolTime = 0; } // 조건이 맞으면 실행할거에요 -> 추격 중이었다면 경로를 취소하고 순찰을 준비하기를
|
||||
Patrol(); // 함수를 실행할거에요 -> 순찰을 도는 Patrol을
|
||||
UpdateMovementAnimation(); // 함수를 실행할거에요 -> 애니메이션 갱신을
|
||||
if (proj != null) proj.Initialize(transform.forward, projectileSpeed, attackDamage); // 초기화할거에요 -> 직선 발사로
|
||||
}
|
||||
}
|
||||
|
||||
void HandleCombat(float dist) // 함수를 선언할거에요 -> 거리별 전투 행동을 정하는 HandleCombat을
|
||||
{
|
||||
if (attackStyle == AttackStyle.Straight && dist < minDistance) RetreatFromPlayer(); // 조건이 맞으면 실행할거에요 -> 직선 공격 타입이고 너무 가까우면 도망가기를
|
||||
else if (dist <= attackRange) TryAttack(); // 조건이 맞으면 실행할거에요 -> 사거리 안이라면 공격 시도를
|
||||
else { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(playerTransform.position); UpdateMovementAnimation(); } } // 그 외엔 실행할거에요 -> 플레이어를 향해 추격하기를
|
||||
}
|
||||
|
||||
void TryAttack() // 함수를 선언할거에요 -> 공격을 수행하는 TryAttack을
|
||||
{
|
||||
if (Time.time < lastAttackTime + attackDelay) return; // 조건이 맞으면 중단할거에요 -> 쿨타임이 안 지났다면
|
||||
lastAttackTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 공격 시간으로
|
||||
isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로
|
||||
|
||||
if (agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 조건이 맞으면 실행할거에요 -> 이동을 멈추고 제자리에 서기를
|
||||
|
||||
Vector3 lookDir = (playerTransform.position - transform.position).normalized; // 벡터를 계산할거에요 -> 플레이어를 바라보는 방향을
|
||||
lookDir.y = 0; // 값을 바꿀거에요 -> 높이 차이를 무시하게
|
||||
if (lookDir != Vector3.zero) transform.rotation = Quaternion.LookRotation(lookDir); // 회전시킬거에요 -> 플레이어 쪽을 보도록
|
||||
|
||||
animator.Play(attackAnim); // 재생할거에요 -> 공격 애니메이션을
|
||||
}
|
||||
|
||||
public override void OnAttackStart() // 함수를 덮어씌워 실행할거에요 -> 공격 타이밍(애니메이션 이벤트)에 호출될 OnAttackStart를
|
||||
{
|
||||
if (!projectilePrefab || !firePoint) return; // 조건이 맞으면 중단할거에요 -> 프리팹이나 발사 위치가 없다면
|
||||
|
||||
GameObject obj = Instantiate(projectilePrefab, firePoint.position, transform.rotation); // 생성할거에요 -> 투사체를 발사 위치에
|
||||
|
||||
if (obj.TryGetComponent<Projectile>(out var proj)) // 조건이 맞으면 실행할거에요 -> 투사체 스크립트가 있다면
|
||||
else // 조건이 틀리면 실행할거에요 -> 곡사형이면
|
||||
{
|
||||
float speed = (attackStyle == AttackStyle.Straight) ? projectileSpeed : 0f; // 값을 결정할거에요 -> 직선이면 속도를, 곡사면 0(물리)을
|
||||
proj.Initialize(transform.forward, speed, attackDamage); // 초기화할거에요 -> 방향, 속도, 데미지 정보를 전달해서
|
||||
}
|
||||
|
||||
if (attackStyle == AttackStyle.Lob) // 조건이 맞으면 실행할거에요 -> 곡사 공격 타입이라면
|
||||
{
|
||||
Rigidbody rb = obj.GetComponent<Rigidbody>(); // 컴포넌트를 가져올거에요 -> 투사체의 리지드바디를
|
||||
if (rb) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
|
||||
Rigidbody rb = obj.GetComponent<Rigidbody>(); // 가져올거에요 -> 리지드바디를
|
||||
if (rb != null)
|
||||
{
|
||||
rb.useGravity = true; // 기능을 켤거에요 -> 중력 영향을 받도록
|
||||
if (proj != null) proj.Initialize(Vector3.zero, 0, attackDamage); // 초기화할거에요 -> 물리 이동으로
|
||||
rb.useGravity = true; // 켤거에요 -> 중력을
|
||||
Vector3 targetPos = playerTransform.position + Vector3.up * aimHeightOffset; // 계산할거에요 -> 타겟 위치를
|
||||
Vector3 velocity = ProjectileMath.CalculateLobVelocity(firePoint.position, targetPos, launchAngle); // 계산할거에요 -> 헬퍼로 속도를
|
||||
|
||||
// ⭐ 목표 지점 설정 (플레이어 위치 + 높이 보정)
|
||||
Vector3 targetPos = playerTransform.position + Vector3.up * aimHeight; // 위치를 계산할거에요 -> 플레이어 위치에 조준 높이를 더해서
|
||||
|
||||
if (usePreciseLob) // 조건이 맞으면 실행할거에요 -> 정밀 곡사를 사용한다면
|
||||
{
|
||||
// 보정된 위치(targetPos)를 기준으로 탄도 계산
|
||||
Vector3 velocity = CalculateLobVelocity(firePoint.position, targetPos, launchAngle); // 계산할거에요 -> 목표에 도달하기 위한 물리 속도를
|
||||
|
||||
if (!float.IsNaN(velocity.x)) // 조건이 맞으면 실행할거에요 -> 계산 결과가 유효하다면
|
||||
{
|
||||
rb.velocity = velocity; // 값을 넣을거에요 -> 계산된 속도를 리지드바디에
|
||||
}
|
||||
else // 조건이 틀리면 실행할거에요 -> 계산 실패(도달 불가)라면
|
||||
{
|
||||
// 계산 실패 시 백업
|
||||
rb.AddForce(transform.forward * throwForce + Vector3.up * throwUpward, ForceMode.Impulse); // 힘을 가할거에요 -> 기본 힘으로 던지기를
|
||||
}
|
||||
}
|
||||
else // 조건이 틀리면 실행할거에요 -> 정밀 곡사가 아니라면
|
||||
{
|
||||
Vector3 dir = aimAtPlayer ? (targetPos - firePoint.position).normalized : transform.forward; // 방향을 결정할거에요 -> 타겟 방향 혹은 정면으로
|
||||
rb.AddForce(dir * throwForce + Vector3.up * throwUpward, ForceMode.Impulse); // 힘을 가할거에요 -> 방향과 힘을 적용해서 던지기를
|
||||
}
|
||||
if (velocity != Vector3.zero) rb.velocity = velocity; // 적용할거에요 -> 속도를
|
||||
else rb.AddForce(transform.forward * 15f + Vector3.up * 5f, ForceMode.Impulse); // 실패 시 기본 힘을
|
||||
}
|
||||
|
||||
if (handModel != null) { handModel.SetActive(false); StartCoroutine(ReloadRoutine()); } // 조건이 맞으면 실행할거에요 -> 손에 있는 무기를 숨기고 장전 코루틴을 시작하기를
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 CalculateLobVelocity(Vector3 origin, Vector3 target, float angle) // 함수를 선언할거에요 -> 곡사 탄도 속도를 계산하는 CalculateLobVelocity를
|
||||
{
|
||||
Vector3 dir = target - origin; // 벡터를 계산할거에요 -> 목표까지의 거리 벡터를
|
||||
float height = dir.y; // 값을 저장할거에요 -> 높이 차이를
|
||||
dir.y = 0; // 값을 바꿀거에요 -> 수평 거리 계산을 위해 y를 0으로
|
||||
float dist = dir.magnitude; // 값을 저장할거에요 -> 수평 거리를
|
||||
|
||||
float a = angle * Mathf.Deg2Rad; // 값을 변환할거에요 -> 각도를 라디안으로
|
||||
|
||||
dir.y = dist * Mathf.Tan(a); // 값을 계산할거에요 -> 탄젠트를 이용해 높이를
|
||||
dist += height / Mathf.Tan(a); // 값을 보정할거에요 -> 높이 차이에 따른 거리 보정을
|
||||
|
||||
float gravity = Physics.gravity.magnitude; // 값을 가져올거에요 -> 중력 가속도를
|
||||
// 물리 공식: v^2 = (g * x^2) / (2 * cos^2(a) * (x * tan(a) - y))
|
||||
float velocitySq = (gravity * dist * dist) / (2 * Mathf.Pow(Mathf.Cos(a), 2) * (dist * Mathf.Tan(a) - height)); // 값을 계산할거에요 -> 필요한 속도의 제곱을
|
||||
|
||||
if (velocitySq <= 0) return Vector3.zero; // 조건이 맞으면 반환할거에요 -> 계산 불가라면 0 벡터를
|
||||
|
||||
float velocity = Mathf.Sqrt(velocitySq); // 값을 계산할거에요 -> 제곱근을 구해서 실제 속도를
|
||||
return dir.normalized * velocity; // 반환할거에요 -> 방향 벡터에 속도를 곱해서
|
||||
}
|
||||
|
||||
void RetreatFromPlayer() // 함수를 선언할거에요 -> 플레이어로부터 도망가는 RetreatFromPlayer를
|
||||
{
|
||||
if (agent.pathPending || (agent.remainingDistance > 0.5f && agent.velocity.magnitude > 0.1f)) { UpdateMovementAnimation(); return; } // 조건이 맞으면 중단할거에요 -> 이미 이동 중이라면
|
||||
Vector3 dir = (transform.position - playerTransform.position).normalized; // 벡터를 계산할거에요 -> 플레이어 반대 방향을
|
||||
Vector3 pos = transform.position + dir * fleeDistance; // 위치를 계산할거에요 -> 도망갈 목표 지점을
|
||||
if (NavMesh.SamplePosition(pos, out NavMeshHit hit, 5f, NavMesh.AllAreas)) { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(hit.position); } } // 조건이 맞으면 실행할거에요 -> 유효한 위치라면 그곳으로 이동하기를
|
||||
UpdateMovementAnimation(); // 함수를 실행할거에요 -> 이동 애니메이션 갱신을
|
||||
}
|
||||
|
||||
IEnumerator ReloadRoutine() // 코루틴 함수를 선언할거에요 -> 무기 장전(재생성)을 처리하는 ReloadRoutine을
|
||||
{
|
||||
isReloading = true; // 상태를 바꿀거에요 -> 장전 중 상태를 참으로
|
||||
yield return new WaitForSeconds(reloadTime); // 기다릴거에요 -> 장전 시간만큼
|
||||
if (handModel != null) handModel.SetActive(true); // 조건이 맞으면 실행할거에요 -> 손에 있는 무기를 다시 보이게
|
||||
isReloading = false; // 상태를 바꿀거에요 -> 장전 중 상태를 거짓으로
|
||||
}
|
||||
|
||||
public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 처리를 하는 OnAttackEnd를
|
||||
{
|
||||
isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓으로
|
||||
if (animator != null) animator.Play(Monster_Idle); // 조건이 맞으면 실행할거에요 -> 대기 애니메이션으로 복귀를
|
||||
if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 조건이 맞으면 실행할거에요 -> 살아있다면 휴식 코루틴 시작을
|
||||
}
|
||||
|
||||
void Patrol() // 함수를 선언할거에요 -> 순찰 로직 Patrol을
|
||||
{
|
||||
if (Time.time < nextPatrolTime) { if (agent.velocity.magnitude < 0.1f) animator.Play(Monster_Idle); return; } // 조건이 맞으면 중단할거에요 -> 대기 시간이라면 대기 모션 재생 후 리턴
|
||||
Vector3 randomPoint = transform.position + new Vector3(Random.Range(-patrolRadius, patrolRadius), 0, Random.Range(-patrolRadius, patrolRadius)); // 벡터를 계산할거에요 -> 랜덤 순찰 지점을
|
||||
if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(hit.position); } } // 조건이 맞으면 실행할거에요 -> 유효한 위치라면 이동하기를
|
||||
nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을
|
||||
}
|
||||
|
||||
void UpdateMovementAnimation() // 함수를 선언할거에요 -> 이동 애니메이션 갱신 함수를
|
||||
{
|
||||
if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면
|
||||
if (agent.velocity.magnitude > 0.1f) animator.Play(walkAnim); else animator.Play(Monster_Idle); // 조건에 따라 재생할거에요 -> 걷기 또는 대기 애니메이션을
|
||||
}
|
||||
}
|
||||
66
Assets/Scripts/Enemy/BossAI/Animatordebugdump.cs
Normal file
66
Assets/Scripts/Enemy/BossAI/Animatordebugdump.cs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// [임시 디버그용] 보스 오브젝트에 붙이고 Play하면
|
||||
/// 콘솔에 모든 AnimationClip 이름을 출력합니다.
|
||||
/// 확인 후 이 스크립트는 삭제하세요.
|
||||
/// </summary>
|
||||
public class AnimatorDebugDump : MonoBehaviour
|
||||
{
|
||||
private void Start()
|
||||
{
|
||||
Animator animator = GetComponent<Animator>();
|
||||
if (animator == null)
|
||||
{
|
||||
Debug.LogError("[AnimatorDebug] Animator 컴포넌트가 없습니다!");
|
||||
return;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 1) RuntimeAnimatorController의 모든 AnimationClip 이름
|
||||
// ═══════════════════════════════════════
|
||||
RuntimeAnimatorController rac = animator.runtimeAnimatorController;
|
||||
if (rac != null)
|
||||
{
|
||||
AnimationClip[] clips = rac.animationClips;
|
||||
Debug.Log($"══════ [AnimatorDebug] 총 클립 수: {clips.Length} ══════");
|
||||
for (int i = 0; i < clips.Length; i++)
|
||||
{
|
||||
Debug.Log($" 클립[{i}]: \"{clips[i].name}\" (길이: {clips[i].length:F2}초)");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[AnimatorDebug] RuntimeAnimatorController가 null입니다!");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 2) 현재 레이어 0의 State 이름 확인 (Play 직후)
|
||||
// ═══════════════════════════════════════
|
||||
AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0);
|
||||
Debug.Log($"[AnimatorDebug] 현재 State nameHash: {stateInfo.shortNameHash}, 길이: {stateInfo.length:F2}초");
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 3) 코드에서 사용 중인 애니메이션 이름들 존재 여부 테스트
|
||||
// ═══════════════════════════════════════
|
||||
string[] testNames = new string[]
|
||||
{
|
||||
"Idle", "Roar", "Walk", "Run",
|
||||
"Attack_Swing", "Attack_Slam", "Attack_Throw",
|
||||
"Skill_DashReady", "Skill_Dash", "Skill_JumpUp", "Skill_JumpDown",
|
||||
"Skill_Pickup", "GetDamage", "Die",
|
||||
// FBX 프리픽스 버전도 테스트
|
||||
"Armature|Idle", "Armature|Roar", "Armature|Walk",
|
||||
"Armature|Attack_Throw", "Armature|Skill_Pickup"
|
||||
};
|
||||
|
||||
Debug.Log("══════ [AnimatorDebug] State 존재 여부 테스트 ══════");
|
||||
foreach (string name in testNames)
|
||||
{
|
||||
int hash = Animator.StringToHash(name);
|
||||
bool exists = animator.HasState(0, hash);
|
||||
string status = exists ? "✅ 존재" : "❌ 없음";
|
||||
Debug.Log($" \"{name}\" → {status}");
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemy/BossAI/Animatordebugdump.cs.meta
Normal file
11
Assets/Scripts/Enemy/BossAI/Animatordebugdump.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3ff6696e8923ba24eb91fcac18f67aa5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -12,14 +12,16 @@ MonoBehaviour:
|
|||
m_Script: {fileID: 11500000, guid: 127d57ae132759d4f8233580c8075e39, type: 3}
|
||||
m_Name: BossCounterConfig
|
||||
m_EditorClassIdentifier:
|
||||
dodgeThreshold: 5
|
||||
aimThreshold: 4
|
||||
pierceThreshold: 0.6
|
||||
minShotsForPierceCheck: 3
|
||||
unlockedThresholdMultiplier: 0.8
|
||||
counterWeightBonus: 3
|
||||
counterSubWeightBonus: 2
|
||||
counterDecayTime: 8
|
||||
maxCounterFrequency: 0.25
|
||||
counterCooldown: 5
|
||||
habitChangeRewardRatio: 0.15
|
||||
patterns:
|
||||
- patternName:
|
||||
targetCounter: 0
|
||||
baseWeight: 0
|
||||
weightMultiplier: 0
|
||||
- patternName:
|
||||
targetCounter: 0
|
||||
baseWeight: 0
|
||||
weightMultiplier: 0
|
||||
- patternName:
|
||||
targetCounter: 0
|
||||
baseWeight: 0
|
||||
weightMultiplier: 0
|
||||
|
|
@ -1,75 +1,17 @@
|
|||
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
using System.Collections.Generic; // 리스트를 사용할거에요 -> 제네릭을
|
||||
|
||||
/// <summary>
|
||||
/// 보스 카운터 시스템의 임계치, 가중치, 감소(Decay) 설정을 담는 ScriptableObject.
|
||||
/// Inspector에서 밸런싱을 위해 쉽게 조정할 수 있습니다.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "BossCounterConfig", menuName = "Boss/Counter Config")] // 메뉴를 만들거에요 -> 에셋 생성 메뉴에 "Boss/Counter Config"를
|
||||
public class BossCounterConfig : ScriptableObject // 클래스를 선언할거에요 -> ScriptableObject를 상속받는 BossCounterConfig를
|
||||
[CreateAssetMenu(fileName = "BossCounterConfig", menuName = "Boss/CounterConfig")] // 메뉴를 만들거에요 -> 에셋 생성 메뉴를
|
||||
public class BossCounterConfig : ScriptableObject // 클래스를 선언할거에요 -> 데이터 저장용 SO를
|
||||
{
|
||||
[Header("══ 기본 임계치 (첫 런 / 잠금 해제 전) ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 기본 임계치 (첫 런 / 잠금 해제 전) ══ 을
|
||||
[Tooltip("회피 카운터 발동 조건: 10초 내 회피 횟수")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
public int dodgeThreshold = 5; // 변수를 선언할거에요 -> 회피 횟수 임계값(5)을 dodgeThreshold에
|
||||
|
||||
[Tooltip("조준 카운터 발동 조건: 10초 내 조준 유지 시간(초)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
public float aimThreshold = 4.0f; // 변수를 선언할거에요 -> 조준 시간 임계값(4.0초)을 aimThreshold에
|
||||
|
||||
[Tooltip("관통 카운터 발동 조건: 10초 내 관통 비율")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
[Range(0f, 1f)] // 슬라이더를 표시할거에요 -> 0부터 1 사이로
|
||||
public float pierceThreshold = 0.6f; // 변수를 선언할거에요 -> 관통 비율 임계값(60%)을 pierceThreshold에
|
||||
|
||||
[Tooltip("관통 비율 판단을 위한 최소 발사 횟수")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
public int minShotsForPierceCheck = 3; // 변수를 선언할거에요 -> 최소 발사 횟수(3)를 minShotsForPierceCheck에
|
||||
|
||||
[Header("══ 잠금 해제 후 임계치 감소 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 잠금 해제 후 임계치 감소 ══ 를
|
||||
[Tooltip("잠금 해제된 카운터의 임계치 감소 비율 (0.8 = 20% 낮아짐)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
[Range(0.5f, 1.0f)] // 슬라이더를 표시할거에요 -> 0.5부터 1.0 사이로
|
||||
public float unlockedThresholdMultiplier = 0.8f; // 변수를 선언할거에요 -> 잠금 해제 시 감소 배율(0.8)을 unlockedThresholdMultiplier에
|
||||
|
||||
[Header("══ 가중치 설정 (확률 가중치 방식) ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 가중치 설정 (확률 가중치 방식) ══ 을
|
||||
[Tooltip("카운터 발동 시 해당 카운터 패턴에 추가되는 가중치")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
public float counterWeightBonus = 3f; // 변수를 선언할거에요 -> 카운터 패턴 보너스 가중치(3.0)를 counterWeightBonus에
|
||||
|
||||
[Tooltip("카운터 발동 시 보조 패턴에 추가되는 가중치")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
public float counterSubWeightBonus = 2f; // 변수를 선언할거에요 -> 보조 패턴 보너스 가중치(2.0)를 counterSubWeightBonus에
|
||||
|
||||
[Header("══ Decay(감소) 설정 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ Decay(감소) 설정 ══ 을
|
||||
[Tooltip("카운터 모드 유지 시간(초). 이 시간 동안 조건 미충족 시 비활성화")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
public float counterDecayTime = 8f; // 변수를 선언할거에요 -> 카운터 유지 시간(8.0초)을 counterDecayTime에
|
||||
|
||||
[Header("══ 빈도 제한 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 빈도 제한 ══ 을
|
||||
[Tooltip("카운터 패턴 선택 확률 상한 (0.25 = 최대 25%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
[Range(0.1f, 0.5f)] // 슬라이더를 표시할거에요 -> 0.1부터 0.5 사이로
|
||||
public float maxCounterFrequency = 0.25f; // 변수를 선언할거에요 -> 카운터 최대 빈도(25%)를 maxCounterFrequency에
|
||||
|
||||
[Tooltip("카운터 패턴 발동 후 쿨타임(초)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
public float counterCooldown = 5f; // 변수를 선언할거에요 -> 카운터 재사용 대기시간(5.0초)을 counterCooldown에
|
||||
|
||||
[Header("══ 습관 변경 보상 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 습관 변경 보상 ══ 을
|
||||
[Tooltip("플레이어가 이전 런에서 발동된 카운터 습관을 이번 런에서 안 보이면, 보스 일반 패턴 난이도 감소 비율")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
[Range(0f, 0.3f)] // 슬라이더를 표시할거에요 -> 0부터 0.3 사이로
|
||||
public float habitChangeRewardRatio = 0.15f; // 변수를 선언할거에요 -> 습관 변경 보상 비율(15%)을 habitChangeRewardRatio에
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 임계치 조회 (잠금 해제 여부 반영)
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>현재 유효 임계치 반환 (잠금 해제 시 낮아짐)</summary>
|
||||
public int GetEffectiveDodgeThreshold(bool isUnlocked) // 함수를 선언할거에요 -> 잠금 해제 여부에 따른 회피 임계값을 반환하는 GetEffectiveDodgeThreshold를
|
||||
[System.Serializable] // 직렬화할거에요 -> 인스펙터에 보이게
|
||||
public struct PatternWeight // 구조체를 정의할거에요 -> 패턴 가중치 정보를 담을
|
||||
{
|
||||
if (!isUnlocked) return dodgeThreshold; // 조건이 맞으면 반환할거에요 -> 잠금 상태라면 기본 임계값을
|
||||
return Mathf.Max(2, Mathf.RoundToInt(dodgeThreshold * unlockedThresholdMultiplier)); // 값을 계산해서 반환할거에요 -> 기본값에 배율을 곱하고 최소 2 이상이 되도록
|
||||
public string patternName; // 변수를 선언할거에요 -> 패턴 이름을
|
||||
public CounterType targetCounter; // 변수를 선언할거에요 -> 반응할 카운터 타입을
|
||||
public float baseWeight; // 변수를 선언할거에요 -> 기본 확률을
|
||||
public float weightMultiplier; // 변수를 선언할거에요 -> 카운터당 증가할 확률을
|
||||
}
|
||||
|
||||
public float GetEffectiveAimThreshold(bool isUnlocked) // 함수를 선언할거에요 -> 잠금 해제 여부에 따른 조준 임계값을 반환하는 GetEffectiveAimThreshold를
|
||||
{
|
||||
if (!isUnlocked) return aimThreshold; // 조건이 맞으면 반환할거에요 -> 잠금 상태라면 기본 임계값을
|
||||
return aimThreshold * unlockedThresholdMultiplier; // 값을 계산해서 반환할거에요 -> 기본값에 배율을 곱한 값을
|
||||
}
|
||||
|
||||
public float GetEffectivePierceThreshold(bool isUnlocked) // 함수를 선언할거에요 -> 잠금 해제 여부에 따른 관통 임계값을 반환하는 GetEffectivePierceThreshold를
|
||||
{
|
||||
if (!isUnlocked) return pierceThreshold; // 조건이 맞으면 반환할거에요 -> 잠금 상태라면 기본 임계값을
|
||||
return pierceThreshold * unlockedThresholdMultiplier; // 값을 계산해서 반환할거에요 -> 기본값에 배율을 곱한 값을
|
||||
}
|
||||
public List<PatternWeight> patterns; // 리스트를 선언할거에요 -> 패턴 설정 목록을
|
||||
}
|
||||
|
|
@ -1,93 +1,32 @@
|
|||
using UnityEngine;
|
||||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
using UnityEngine.UI; // UI 기능을 불러올거에요 -> UnityEngine.UI를
|
||||
using System.Text; // 문자열 빌더를 사용할거에요 -> System.Text를
|
||||
|
||||
/// <summary>
|
||||
/// 개발 중 보스 카운터 시스템을 테스트하기 위한 디버그 패널.
|
||||
/// 빌드 시 자동으로 비활성화됩니다.
|
||||
/// </summary>
|
||||
public class BossCounterDebugPanel : MonoBehaviour
|
||||
public class BossCounterDebugPanel : MonoBehaviour // 클래스를 선언할거에요 -> 디버그 패널을
|
||||
{
|
||||
[SerializeField] private BossCounterSystem counterSystem;
|
||||
[SerializeField] private bool showDebugGUI = true;
|
||||
[SerializeField] private BossCounterSystem system; // 변수를 선언할거에요 -> 시스템 참조를
|
||||
[SerializeField] private Text debugText; // 변수를 선언할거에요 -> 텍스트 UI를
|
||||
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
|
||||
private void OnGUI()
|
||||
private void Update() // 함수를 실행할거에요 -> 매 프레임
|
||||
{
|
||||
if (!showDebugGUI) return;
|
||||
if (!system || !debugText) return; // 중단할거에요 -> 참조가 없으면
|
||||
|
||||
GUILayout.BeginArea(new Rect(10, 10, 400, 500));
|
||||
GUILayout.BeginVertical("box");
|
||||
StringBuilder sb = new StringBuilder(); // 생성할거에요 -> 빌더 객체를
|
||||
sb.AppendLine("=== Boss Counter Debug ==="); // 추가할거에요 -> 제목 줄을
|
||||
|
||||
GUILayout.Label("═══ 보스 카운터 디버그 ═══", GUI.skin.box);
|
||||
var counters = system.GetActiveCounters(); // 가져올거에요 -> 활성 카운터 목록을
|
||||
foreach (var pair in counters) // 반복할거에요 -> 각 카운터마다
|
||||
sb.AppendLine($"{pair.Key}: {pair.Value}"); // 추가할거에요 -> 키와 값을
|
||||
|
||||
// ── 플레이어 행동 데이터 ──
|
||||
var tracker = PlayerBehaviorTracker.Instance;
|
||||
if (tracker != null)
|
||||
{
|
||||
GUILayout.Label("── 플레이어 행동 (10초 윈도우) ──");
|
||||
GUILayout.Label($" 회피 횟수: {tracker.DodgeCount}");
|
||||
GUILayout.Label($" 조준 시간: {tracker.AimHoldTime:F1}s");
|
||||
GUILayout.Label($" 관통 비율: {tracker.PierceRatio:P0} ({tracker.TotalShotsInWindow}발)");
|
||||
}
|
||||
// ⭐ [수정] 괄호 () 추가하여 올바르게 메서드를 호출합니다.
|
||||
sb.AppendLine($"Reward: {system.IsHabitChangeRewarded()}"); // 추가할거에요 -> 보상 상태를
|
||||
|
||||
GUILayout.Space(5);
|
||||
|
||||
// ── 카운터 상태 ──
|
||||
if (counterSystem != null)
|
||||
{
|
||||
GUILayout.Label("── 카운터 모드 ──");
|
||||
var actives = counterSystem.GetActiveCounters();
|
||||
GUILayout.Label($" 활성 카운터: {(actives.Count > 0 ? string.Join(", ", actives) : "없음")}");
|
||||
GUILayout.Label($" 습관 변경 보상: {(counterSystem.IsHabitChangeRewarded ? "ON" : "OFF")}");
|
||||
}
|
||||
|
||||
GUILayout.Space(5);
|
||||
|
||||
// ── 영구 데이터 ──
|
||||
var persistence = BossCounterPersistence.Instance;
|
||||
if (persistence != null)
|
||||
{
|
||||
GUILayout.Label("── 영구 잠금 해제 ──");
|
||||
GUILayout.Label($" 회피: {(persistence.IsUnlocked(CounterType.Dodge) ? "✓" : "✗")} (발동 {persistence.Data.dodgeCounterActivations}회)");
|
||||
GUILayout.Label($" 조준: {(persistence.IsUnlocked(CounterType.Aim) ? "✓" : "✗")} (발동 {persistence.Data.aimCounterActivations}회)");
|
||||
GUILayout.Label($" 관통: {(persistence.IsUnlocked(CounterType.Pierce) ? "✓" : "✗")} (발동 {persistence.Data.pierceCounterActivations}회)");
|
||||
}
|
||||
|
||||
GUILayout.Space(10);
|
||||
|
||||
// ── 수동 트리거 버튼 ──
|
||||
GUILayout.Label("── 수동 테스트 ──");
|
||||
|
||||
if (tracker != null)
|
||||
{
|
||||
GUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("회피 x5")) { for (int i = 0; i < 5; i++) tracker.RecordDodge(); }
|
||||
if (GUILayout.Button("발사(일반)")) { tracker.RecordShot(false); }
|
||||
if (GUILayout.Button("발사(관통)")) { tracker.RecordShot(true); }
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
if (counterSystem != null)
|
||||
{
|
||||
if (GUILayout.Button("패턴 선택 테스트"))
|
||||
{
|
||||
string pattern = counterSystem.SelectBossPattern();
|
||||
Debug.Log($"[디버그] 선택된 패턴: {pattern}");
|
||||
}
|
||||
}
|
||||
|
||||
if (persistence != null)
|
||||
{
|
||||
GUILayout.Space(5);
|
||||
if (GUILayout.Button("영구 데이터 초기화"))
|
||||
{
|
||||
persistence.ResetAllData();
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.EndVertical();
|
||||
GUILayout.EndArea();
|
||||
debugText.text = sb.ToString(); // 적용할거에요 -> 텍스트 UI에
|
||||
}
|
||||
|
||||
#endif
|
||||
public void OnClickClearData() // 함수를 선언할거에요 -> 초기화 버튼용
|
||||
{
|
||||
if (BossCounterPersistence.Instance) // 조건이 맞으면 실행할거에요 -> 인스턴스가 있다면
|
||||
BossCounterPersistence.Instance.ResetAllData(); // 실행할거에요 -> 데이터 삭제 함수를
|
||||
}
|
||||
}
|
||||
|
|
@ -1,185 +1,60 @@
|
|||
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
||||
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||
using UnityEngine.UI; // UI 기능을 사용할거에요 -> UnityEngine.UI를
|
||||
using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를
|
||||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
using UnityEngine.UI; // UI 기능을 불러올거에요 -> UnityEngine.UI를
|
||||
using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
|
||||
|
||||
/// <summary>
|
||||
/// 보스 카운터 시스템의 연출/UI를 담당합니다.
|
||||
/// </summary>
|
||||
public class BossCounterFeedback : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossCounterFeedback을
|
||||
public class BossCounterFeedback : MonoBehaviour // 클래스를 선언할거에요 -> 피드백 시스템을
|
||||
{
|
||||
[Header("참조")] // 인스펙터 창에 제목을 표시할거에요 -> 참조 를
|
||||
[SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 카운터 시스템 스크립트를 연결할 counterSystem을
|
||||
[SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 시스템 연결을
|
||||
[SerializeField] private Text feedbackText; // 변수를 선언할거에요 -> 텍스트 UI를
|
||||
[SerializeField] private float displayDuration = 3f; // 변수를 선언할거에요 -> 표시 시간을
|
||||
|
||||
[Header("UI 요소 (선택)")] // 인스펙터 창에 제목을 표시할거에요 -> UI 요소 (선택) 을
|
||||
[SerializeField] private TextMeshProUGUI bossDialogueText; // 변수를 선언할거에요 -> 보스 대사를 표시할 텍스트 UI를
|
||||
[SerializeField] private CanvasGroup dialogueCanvasGroup; // 변수를 선언할거에요 -> 대사창의 투명도를 조절할 캔버스 그룹을
|
||||
[SerializeField] private float dialogueDisplayDuration = 3f; // 변수를 선언할거에요 -> 대사 표시 시간(3초)을 dialogueDisplayDuration에
|
||||
[SerializeField] private float dialogueFadeDuration = 0.5f; // 변수를 선언할거에요 -> 대사 페이드 시간(0.5초)을 dialogueFadeDuration에
|
||||
|
||||
[Header("카운터별 보스 대사")] // 인스펙터 창에 제목을 표시할거에요 -> 카운터별 보스 대사 를
|
||||
[SerializeField]
|
||||
private string[] dodgeCounterDialogues = new string[] // 배열을 초기화할거에요 -> 회피 카운터 대사 목록을
|
||||
private void OnEnable() // 함수를 실행할거에요 -> 활성화 시
|
||||
{
|
||||
"...또 도망치려는 건가.",
|
||||
"네 발은 이미 읽었다.",
|
||||
"아무리 피해도 소용없어."
|
||||
};
|
||||
|
||||
[SerializeField]
|
||||
private string[] aimCounterDialogues = new string[] // 배열을 초기화할거에요 -> 조준 카운터 대사 목록을
|
||||
{
|
||||
"그렇게 오래 노려봐야...",
|
||||
"느린 조준은 빈틈이지.",
|
||||
"시간을 줄 생각은 없다."
|
||||
};
|
||||
|
||||
[SerializeField]
|
||||
private string[] pierceCounterDialogues = new string[] // 배열을 초기화할거에요 -> 관통 카운터 대사 목록을
|
||||
{
|
||||
"그 화살은 더 이상 통하지 않아.",
|
||||
"관통? 이번엔 막아주지.",
|
||||
"같은 수를 반복하다니."
|
||||
};
|
||||
|
||||
[SerializeField]
|
||||
private string[] habitChangeDialogues = new string[] // 배열을 초기화할거에요 -> 습관 변경 보상 대사 목록을
|
||||
{
|
||||
"...다른 수를 쓰는 건가.",
|
||||
"흥, 조금은 배운 모양이군.",
|
||||
"이번엔 다르군... 재밌어."
|
||||
};
|
||||
|
||||
// ── 잠금 해제 여부에 따른 첫 발동 대사 ──
|
||||
[Header("첫 잠금 해제 시 대사 (서사 강조)")] // 인스펙터 창에 제목을 표시할거에요 -> 첫 잠금 해제 시 대사 (서사 강조) 를
|
||||
[SerializeField]
|
||||
private string[] firstUnlockDialogues = new string[] // 배열을 초기화할거에요 -> 첫 해금 시 출력할 대사 목록을
|
||||
{
|
||||
"...기억했다. 네 버릇을.",
|
||||
"이제 알겠어. 네 전투 방식이.",
|
||||
"한 번이면 충분해. 패턴을 읽었다."
|
||||
};
|
||||
|
||||
private Coroutine dialogueCoroutine; // 변수를 선언할거에요 -> 실행 중인 대사 코루틴을 저장할 dialogueCoroutine을
|
||||
|
||||
private void OnEnable() // 함수를 실행할거에요 -> 오브젝트 활성화 시 호출되는 OnEnable을
|
||||
{
|
||||
if (counterSystem != null) // 조건이 맞으면 실행할거에요 -> 카운터 시스템이 연결되어 있다면
|
||||
if (counterSystem) // 조건이 맞으면 실행할거에요 -> 시스템이 연결되어 있다면
|
||||
{
|
||||
counterSystem.OnCounterActivated.AddListener(OnCounterActivated); // 구독할거에요 -> 카운터 활성 이벤트에 OnCounterActivated를
|
||||
counterSystem.OnCounterDeactivated.AddListener(OnCounterDeactivated); // 구독할거에요 -> 카운터 비활성 이벤트에 OnCounterDeactivated를
|
||||
counterSystem.OnHabitChangeRewarded.AddListener(OnHabitChangeRewarded); // 구독할거에요 -> 보상 이벤트에 OnHabitChangeRewarded를
|
||||
// ⭐ 매개변수 타입(string)이 일치하므로 이제 에러가 나지 않습니다.
|
||||
counterSystem.OnCounterActivated.AddListener(ShowActivation); // 구독할거에요 -> 활성화 알림을
|
||||
counterSystem.OnCounterDeactivated.AddListener(ShowDeactivation); // 구독할거에요 -> 비활성화 알림을
|
||||
counterSystem.OnHabitChangeRewarded.AddListener(ShowReward); // 구독할거에요 -> 보상 알림을
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable() // 함수를 실행할거에요 -> 오브젝트 비활성화 시 호출되는 OnDisable을
|
||||
private void OnDisable() // 함수를 실행할거에요 -> 비활성화 시
|
||||
{
|
||||
if (counterSystem != null) // 조건이 맞으면 실행할거에요 -> 카운터 시스템이 연결되어 있다면
|
||||
if (counterSystem) // 조건이 맞으면 실행할거에요 -> 시스템이 연결되어 있다면
|
||||
{
|
||||
counterSystem.OnCounterActivated.RemoveListener(OnCounterActivated); // 구독 해제할거에요 -> 카운터 활성 이벤트를
|
||||
counterSystem.OnCounterDeactivated.RemoveListener(OnCounterDeactivated); // 구독 해제할거에요 -> 카운터 비활성 이벤트를
|
||||
counterSystem.OnHabitChangeRewarded.RemoveListener(OnHabitChangeRewarded); // 구독 해제할거에요 -> 보상 이벤트를
|
||||
counterSystem.OnCounterActivated.RemoveListener(ShowActivation); // 해지할거에요 -> 구독을
|
||||
counterSystem.OnCounterDeactivated.RemoveListener(ShowDeactivation); // 해지할거에요 -> 구독을
|
||||
counterSystem.OnHabitChangeRewarded.RemoveListener(ShowReward); // 해지할거에요 -> 구독을
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 이벤트 핸들러
|
||||
// ═══════════════════════════════════════════
|
||||
// ⭐ [수정] string 매개변수 추가 (이벤트 시그니처 일치)
|
||||
private void ShowActivation(string id) => ShowMsg($"⚠️ 패턴 감지: {id} 증가!"); // 실행할거에요 -> 메시지 출력을
|
||||
private void ShowDeactivation(string id) => ShowMsg($"✅ 패턴 해소: {id} 감소"); // 실행할거에요 -> 메시지 출력을
|
||||
private void ShowReward(string id) => ShowMsg($"🎉 습관 보상 획득! ({id})"); // 실행할거에요 -> 메시지 출력을
|
||||
|
||||
private void OnCounterActivated(CounterType type) // 함수를 선언할거에요 -> 카운터 활성 시 호출될 OnCounterActivated를
|
||||
private void ShowMsg(string msg) // 함수를 선언할거에요 -> 텍스트 출력 로직을
|
||||
{
|
||||
var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소 인스턴스를 persistence에
|
||||
|
||||
// 첫 잠금 해제인지 확인 (총 발동 횟수가 1이면 방금 처음 잠금 해제된 것)
|
||||
bool isFirstUnlock = false; // 변수를 초기화할거에요 -> 첫 해금 여부를 거짓(false)으로
|
||||
if (persistence != null) // 조건이 맞으면 실행할거에요 -> 저장소가 있다면
|
||||
if (feedbackText) // 조건이 맞으면 실행할거에요 -> 텍스트 컴포넌트가 있다면
|
||||
{
|
||||
int activations = type switch // 값을 가져올거에요 -> 타입에 따른 발동 횟수를
|
||||
{
|
||||
CounterType.Dodge => persistence.Data.dodgeCounterActivations,
|
||||
CounterType.Aim => persistence.Data.aimCounterActivations,
|
||||
CounterType.Pierce => persistence.Data.pierceCounterActivations,
|
||||
_ => 0
|
||||
};
|
||||
isFirstUnlock = (activations <= 1); // 판단할거에요 -> 발동 횟수가 1 이하라면 첫 해금이라고
|
||||
feedbackText.text = msg; // 넣을거에요 -> 메시지 내용을
|
||||
feedbackText.gameObject.SetActive(true); // 켤거에요 -> 텍스트 오브젝트를
|
||||
StopAllCoroutines(); // 중단할거에요 -> 이전 코루틴을
|
||||
StartCoroutine(HideRoutine()); // 시작할거에요 -> 숨기기 타이머를
|
||||
}
|
||||
|
||||
// 대사 선택
|
||||
string dialogue; // 변수를 선언할거에요 -> 출력할 대사를 담을 dialogue를
|
||||
if (isFirstUnlock) // 조건이 맞으면 실행할거에요 -> 첫 해금이라면
|
||||
{
|
||||
dialogue = firstUnlockDialogues[Random.Range(0, firstUnlockDialogues.Length)]; // 선택할거에요 -> 첫 해금 대사 중 하나를 랜덤으로
|
||||
}
|
||||
else // 조건이 틀리면 실행할거에요 -> 이미 해금된 상태라면
|
||||
{
|
||||
dialogue = type switch // 분기할거에요 -> 카운터 타입에 따라
|
||||
{
|
||||
CounterType.Dodge => dodgeCounterDialogues[Random.Range(0, dodgeCounterDialogues.Length)], // 선택할거에요 -> 회피 대사를
|
||||
CounterType.Aim => aimCounterDialogues[Random.Range(0, aimCounterDialogues.Length)], // 선택할거에요 -> 조준 대사를
|
||||
CounterType.Pierce => pierceCounterDialogues[Random.Range(0, pierceCounterDialogues.Length)], // 선택할거에요 -> 관통 대사를
|
||||
_ => "" // 기본값은 빈 문자열로
|
||||
};
|
||||
}
|
||||
|
||||
ShowDialogue(dialogue); // 함수를 실행할거에요 -> 선택된 대사를 화면에 띄우는 ShowDialogue를
|
||||
|
||||
Debug.Log($"[BossCounterFeedback] {type} 카운터 연출 재생" + (isFirstUnlock ? " (첫 잠금 해제!)" : "")); // 로그를 출력할거에요 -> 연출 재생 알림을
|
||||
Debug.Log($"[Feedback] {msg}"); // 로그를 찍을거에요 -> 콘솔 확인용으로
|
||||
}
|
||||
|
||||
private void OnCounterDeactivated(CounterType type) // 함수를 선언할거에요 -> 카운터 종료 시 호출될 OnCounterDeactivated를
|
||||
private IEnumerator HideRoutine() // 코루틴 함수를 정의할거에요 -> 숨기기 타이머를
|
||||
{
|
||||
// 카운터 해제 시 연출 (이펙트 종료 등)
|
||||
Debug.Log($"[BossCounterFeedback] {type} 카운터 연출 종료"); // 로그를 출력할거에요 -> 연출 종료 알림을
|
||||
yield return new WaitForSeconds(displayDuration); // 기다릴거에요 -> 설정된 시간만큼
|
||||
if (feedbackText) feedbackText.gameObject.SetActive(false); // 끌거에요 -> 텍스트를
|
||||
}
|
||||
|
||||
private void OnHabitChangeRewarded() // 함수를 선언할거에요 -> 보상 지급 시 호출될 OnHabitChangeRewarded를
|
||||
public void OnClickReset() // 함수를 선언할거에요 -> 리셋 버튼 기능을
|
||||
{
|
||||
string dialogue = habitChangeDialogues[Random.Range(0, habitChangeDialogues.Length)]; // 선택할거에요 -> 보상 대사 중 하나를 랜덤으로
|
||||
ShowDialogue(dialogue); // 함수를 실행할거에요 -> 대사를 띄우는 ShowDialogue를
|
||||
|
||||
Debug.Log("[BossCounterFeedback] 습관 변경 보상 연출!"); // 로그를 출력할거에요 -> 보상 연출 알림을
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 대사 표시
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
private void ShowDialogue(string text) // 함수를 선언할거에요 -> 텍스트를 UI에 표시하는 ShowDialogue를
|
||||
{
|
||||
if (bossDialogueText == null || dialogueCanvasGroup == null) return; // 조건이 맞으면 중단할거에요 -> UI 요소가 연결되지 않았다면
|
||||
|
||||
if (dialogueCoroutine != null) // 조건이 맞으면 실행할거에요 -> 이미 실행 중인 대사가 있다면
|
||||
StopCoroutine(dialogueCoroutine); // 중단할거에요 -> 기존 코루틴을
|
||||
|
||||
dialogueCoroutine = StartCoroutine(DialogueRoutine(text)); // 코루틴을 시작할거에요 -> 새 대사를 출력하는 DialogueRoutine을
|
||||
}
|
||||
|
||||
private IEnumerator DialogueRoutine(string text) // 코루틴 함수를 선언할거에요 -> 페이드 효과와 함께 대사를 출력하는 DialogueRoutine을
|
||||
{
|
||||
bossDialogueText.text = text; // 값을 설정할거에요 -> UI 텍스트 내용을 전달받은 text로
|
||||
dialogueCanvasGroup.alpha = 0f; // 값을 설정할거에요 -> 투명도를 0(안 보임)으로
|
||||
|
||||
// 페이드 인
|
||||
float t = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로
|
||||
while (t < dialogueFadeDuration) // 반복할거에요 -> 경과 시간이 페이드 시간보다 작을 동안
|
||||
{
|
||||
t += Time.deltaTime; // 값을 더할거에요 -> 경과 시간에 프레임 시간을
|
||||
dialogueCanvasGroup.alpha = t / dialogueFadeDuration; // 값을 조절할거에요 -> 투명도를 서서히 1로
|
||||
yield return null; // 대기할거에요 -> 다음 프레임까지
|
||||
}
|
||||
dialogueCanvasGroup.alpha = 1f; // 값을 확정할거에요 -> 투명도를 완전 불투명(1)으로
|
||||
|
||||
// 유지
|
||||
yield return new WaitForSeconds(dialogueDisplayDuration); // 기다릴거에요 -> 대사 유지 시간만큼
|
||||
|
||||
// 페이드 아웃
|
||||
t = 0f; // 변수를 초기화할거에요 -> 경과 시간을 다시 0으로
|
||||
while (t < dialogueFadeDuration) // 반복할거에요 -> 경과 시간이 페이드 시간보다 작을 동안
|
||||
{
|
||||
t += Time.deltaTime; // 값을 더할거에요 -> 경과 시간에 프레임 시간을
|
||||
dialogueCanvasGroup.alpha = 1f - (t / dialogueFadeDuration); // 값을 조절할거에요 -> 투명도를 서서히 0으로
|
||||
yield return null; // 대기할거에요 -> 다음 프레임까지
|
||||
}
|
||||
dialogueCanvasGroup.alpha = 0f; // 값을 확정할거에요 -> 투명도를 완전 투명(0)으로
|
||||
if (BossCounterPersistence.Instance) // 조건이 맞으면 실행할거에요 -> 인스턴스가 존재하면
|
||||
BossCounterPersistence.Instance.ResetAllData(); // 실행할거에요 -> 데이터 초기화를
|
||||
}
|
||||
}
|
||||
|
|
@ -1,129 +1,55 @@
|
|||
using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
|
||||
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
using System.Collections.Generic; // 딕셔너리를 사용할거에요 -> System.Collections.Generic을
|
||||
|
||||
/// <summary>
|
||||
/// 런 간 영구 저장되는 보스 카운터 잠금 해제 데이터.
|
||||
/// </summary>
|
||||
[System.Serializable] // 직렬화할거에요 -> 인스펙터나 JSON으로 저장 가능하게
|
||||
public class BossCounterSaveData // 클래스를 선언할거에요 -> 저장할 데이터 구조체인 BossCounterSaveData를
|
||||
public class BossCounterPersistence : MonoBehaviour // 클래스를 선언할거에요 -> 데이터 저장을 담당하는 BossCounterPersistence를
|
||||
{
|
||||
public bool dodgeCounterUnlocked = false; // 변수를 선언할거에요 -> 회피 카운터 해금 여부를
|
||||
public bool aimCounterUnlocked = false; // 변수를 선언할거에요 -> 조준 카운터 해금 여부를
|
||||
public bool pierceCounterUnlocked = false; // 변수를 선언할거에요 -> 관통 카운터 해금 여부를
|
||||
public static BossCounterPersistence Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스를
|
||||
|
||||
/// <summary>각 카운터가 발동된 총 횟수</summary>
|
||||
public int dodgeCounterActivations = 0; // 변수를 선언할거에요 -> 회피 카운터 총 발동 횟수를
|
||||
public int aimCounterActivations = 0; // 변수를 선언할거에요 -> 조준 카운터 총 발동 횟수를
|
||||
public int pierceCounterActivations = 0; // 변수를 선언할거에요 -> 관통 카운터 총 발동 횟수를
|
||||
}
|
||||
// ⭐ [복구] 외부(DebugPanel 등)에서 참조하던 데이터 저장소
|
||||
public Dictionary<CounterType, int> Data { get; private set; } = new Dictionary<CounterType, int>(); // 프로퍼티를 선언할거에요 -> 카운터 데이터를 담을 딕셔너리를
|
||||
|
||||
public class BossCounterPersistence : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossCounterPersistence를
|
||||
{
|
||||
public static BossCounterPersistence Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스 접근용 Instance를
|
||||
|
||||
private const string SAVE_KEY = "BossCounterData"; // 상수를 정의할거에요 -> 저장에 사용할 키 이름("BossCounterData")을 SAVE_KEY에
|
||||
|
||||
public BossCounterSaveData Data { get; private set; } // 프로퍼티를 선언할거에요 -> 실제 저장 데이터를 담을 Data를
|
||||
|
||||
private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 호출되는 Awake를
|
||||
private void Awake() // 함수를 실행할거에요 -> 초기화 Awake를
|
||||
{
|
||||
if (Instance != null && Instance != this) // 조건이 맞으면 실행할거에요 -> 이미 다른 인스턴스가 존재한다면
|
||||
if (Instance == null) Instance = this; // 조건이 맞으면 설정할거에요 -> 나를 유일한 인스턴스로
|
||||
else Destroy(gameObject); // 아니면 파괴할거에요 -> 중복된 나를
|
||||
}
|
||||
|
||||
public void SaveData(Dictionary<CounterType, int> counters) // 함수를 선언할거에요 -> 저장을 수행하는 SaveData를
|
||||
{
|
||||
Data = counters; // 갱신할거에요 -> 내부 데이터 프로퍼티를
|
||||
foreach (var pair in counters) // 반복할거에요 -> 모든 카운터를
|
||||
{
|
||||
Destroy(gameObject); // 파괴할거에요 -> 중복된 이 오브젝트를
|
||||
return; // 중단할거에요 -> 초기화 로직을
|
||||
PlayerPrefs.SetInt($"BossCounter_{pair.Key}", pair.Value); // 저장할거에요 -> PlayerPrefs에 키와 값을
|
||||
}
|
||||
Instance = this; // 값을 저장할거에요 -> 내 자신(this)을 싱글톤 인스턴스에
|
||||
DontDestroyOnLoad(gameObject); // 유지할거에요 -> 씬이 바뀌어도 이 오브젝트를 파괴하지 않게
|
||||
Load(); // 함수를 실행할거에요 -> 저장된 데이터를 불러오는 Load를
|
||||
PlayerPrefs.Save(); // 확정할거에요 -> 저장을
|
||||
Debug.Log("💾 [BossPersistence] 데이터 저장됨"); // 로그를 찍을거에요 -> 저장 완료 메시지를
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 잠금 해제
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>특정 카운터 타입을 영구 잠금 해제</summary>
|
||||
public void UnlockCounter(CounterType type) // 함수를 선언할거에요 -> 카운터를 해금하는 UnlockCounter를
|
||||
public void LoadData(Dictionary<CounterType, int> counters) // 함수를 선언할거에요 -> 불러오기를 수행하는 LoadData를
|
||||
{
|
||||
switch (type) // 분기할거에요 -> 카운터 타입(type)에 따라
|
||||
foreach (CounterType type in System.Enum.GetValues(typeof(CounterType))) // 반복할거에요 -> 모든 타입을 돌면서
|
||||
{
|
||||
case CounterType.Dodge: // 조건이 맞으면 실행할거에요 -> 회피 타입이라면
|
||||
if (!Data.dodgeCounterUnlocked) // 조건이 맞으면 실행할거에요 -> 아직 해금되지 않았다면
|
||||
{
|
||||
Data.dodgeCounterUnlocked = true; // 값을 바꿀거에요 -> 회피 해금 상태를 참(true)으로
|
||||
Debug.Log("[BossCounter] 회피 카운터 영구 잠금 해제!"); // 로그를 출력할거에요 -> 해금 알림 메시지를
|
||||
}
|
||||
break;
|
||||
case CounterType.Aim: // 조건이 맞으면 실행할거에요 -> 조준 타입이라면
|
||||
if (!Data.aimCounterUnlocked) // 조건이 맞으면 실행할거에요 -> 아직 해금되지 않았다면
|
||||
{
|
||||
Data.aimCounterUnlocked = true; // 값을 바꿀거에요 -> 조준 해금 상태를 참(true)으로
|
||||
Debug.Log("[BossCounter] 조준 카운터 영구 잠금 해제!"); // 로그를 출력할거에요 -> 해금 알림 메시지를
|
||||
}
|
||||
break;
|
||||
case CounterType.Pierce: // 조건이 맞으면 실행할거에요 -> 관통 타입이라면
|
||||
if (!Data.pierceCounterUnlocked) // 조건이 맞으면 실행할거에요 -> 아직 해금되지 않았다면
|
||||
{
|
||||
Data.pierceCounterUnlocked = true; // 값을 바꿀거에요 -> 관통 해금 상태를 참(true)으로
|
||||
Debug.Log("[BossCounter] 관통 카운터 영구 잠금 해제!"); // 로그를 출력할거에요 -> 해금 알림 메시지를
|
||||
}
|
||||
break;
|
||||
string key = $"BossCounter_{type}"; // 키를 만들거에요 -> 저장된 키 이름을
|
||||
int val = PlayerPrefs.GetInt(key, 0); // 불러올거에요 -> 저장된 값을 (없으면 0)
|
||||
|
||||
if (counters.ContainsKey(type)) counters[type] = val; // 조건이 맞으면 갱신할거에요 -> 딕셔너리 값을
|
||||
else counters.Add(type, val); // 아니면 추가할거에요 -> 딕셔너리에 값을
|
||||
}
|
||||
Save(); // 함수를 실행할거에요 -> 변경된 데이터를 저장하는 Save를
|
||||
Data = counters; // 동기화할거에요 -> 내부 데이터 프로퍼티를
|
||||
Debug.Log("📂 [BossPersistence] 데이터 로드됨"); // 로그를 찍을거에요 -> 로드 완료 메시지를
|
||||
}
|
||||
|
||||
/// <summary>카운터가 잠금 해제되어 있는지 확인</summary>
|
||||
public bool IsUnlocked(CounterType type) // 함수를 선언할거에요 -> 해금 여부를 확인하는 IsUnlocked를
|
||||
// ⭐ [복구] 외부에서 호출하는 데이터 초기화 함수
|
||||
public void ResetAllData() // 함수를 선언할거에요 -> 모든 데이터를 삭제하는 ResetAllData를
|
||||
{
|
||||
return type switch // 반환할거에요 -> 타입에 따른 해금 변수 값을
|
||||
{
|
||||
CounterType.Dodge => Data.dodgeCounterUnlocked, // 매칭되면 반환할거에요 -> 회피 해금 여부를
|
||||
CounterType.Aim => Data.aimCounterUnlocked, // 매칭되면 반환할거에요 -> 조준 해금 여부를
|
||||
CounterType.Pierce => Data.pierceCounterUnlocked, // 매칭되면 반환할거에요 -> 관통 해금 여부를
|
||||
_ => false // 그 외에는 반환할거에요 -> 거짓(false)을
|
||||
};
|
||||
PlayerPrefs.DeleteAll(); // 삭제할거에요 -> 저장된 모든 프리팹 데이터를
|
||||
Data.Clear(); // 비울거에요 -> 메모리 상의 데이터를
|
||||
Debug.Log("🗑️ [BossPersistence] 초기화 완료"); // 로그를 찍을거에요 -> 초기화 완료 메시지를
|
||||
}
|
||||
|
||||
/// <summary>카운터 발동 횟수 기록</summary>
|
||||
public void RecordActivation(CounterType type) // 함수를 선언할거에요 -> 발동 횟수를 기록하는 RecordActivation을
|
||||
// ⭐ [복구] 해금 여부 확인 함수
|
||||
public bool IsUnlocked(CounterType type) // 함수를 선언할거에요 -> 해금 여부를 반환하는 IsUnlocked를
|
||||
{
|
||||
switch (type) // 분기할거에요 -> 카운터 타입(type)에 따라
|
||||
{
|
||||
case CounterType.Dodge: Data.dodgeCounterActivations++; break; // 일치하면 실행할거에요 -> 회피 발동 횟수를 1 증가시키기를
|
||||
case CounterType.Aim: Data.aimCounterActivations++; break; // 일치하면 실행할거에요 -> 조준 발동 횟수를 1 증가시키기를
|
||||
case CounterType.Pierce: Data.pierceCounterActivations++; break; // 일치하면 실행할거에요 -> 관통 발동 횟수를 1 증가시키기를
|
||||
}
|
||||
Save(); // 함수를 실행할거에요 -> 변경된 횟수를 저장하는 Save를
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 저장 / 로드 / 리셋
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
public void Save() // 함수를 선언할거에요 -> 데이터를 디스크에 저장하는 Save를
|
||||
{
|
||||
string json = JsonUtility.ToJson(Data); // 변환할거에요 -> 데이터 객체를 JSON 문자열로
|
||||
PlayerPrefs.SetString(SAVE_KEY, json); // 저장할거에요 -> JSON 문자열을 PlayerPrefs에
|
||||
PlayerPrefs.Save(); // 실행할거에요 -> 변경사항을 디스크에 즉시 쓰기를
|
||||
}
|
||||
|
||||
public void Load() // 함수를 선언할거에요 -> 데이터를 불러오는 Load를
|
||||
{
|
||||
if (PlayerPrefs.HasKey(SAVE_KEY)) // 조건이 맞으면 실행할거에요 -> 저장된 키가 존재한다면
|
||||
{
|
||||
string json = PlayerPrefs.GetString(SAVE_KEY); // 불러올거에요 -> 저장된 JSON 문자열을
|
||||
Data = JsonUtility.FromJson<BossCounterSaveData>(json); // 변환할거에요 -> JSON을 데이터 객체로 복구해서 Data에
|
||||
}
|
||||
else // 조건이 틀리면 실행할거에요 -> 저장된 데이터가 없다면
|
||||
{
|
||||
Data = new BossCounterSaveData(); // 생성할거에요 -> 새로운 빈 데이터 객체를
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>모든 영구 데이터 초기화 (디버그/뉴게임+)</summary>
|
||||
public void ResetAllData() // 함수를 선언할거에요 -> 모든 데이터를 초기화하는 ResetAllData를
|
||||
{
|
||||
Data = new BossCounterSaveData(); // 초기화할거에요 -> 데이터 객체를 새 것으로
|
||||
PlayerPrefs.DeleteKey(SAVE_KEY); // 삭제할거에요 -> 저장된 키를
|
||||
Debug.Log("[BossCounter] 모든 보스 기억 데이터 초기화됨"); // 로그를 출력할거에요 -> 초기화 완료 메시지를
|
||||
return Data.ContainsKey(type) && Data[type] > 0; // 반환할거에요 -> 데이터가 있고 0보다 큰지 여부를
|
||||
}
|
||||
}
|
||||
|
|
@ -1,373 +1,64 @@
|
|||
using System.Collections.Generic; // 리스트와 딕셔너리 기능을 사용할거에요 -> System.Collections.Generic을
|
||||
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||
using UnityEngine.Events; // 유니티 이벤트 기능을 사용할거에요 -> UnityEngine.Events를
|
||||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
using System.Collections.Generic; // 딕셔너리를 사용할거에요 -> System.Collections.Generic을
|
||||
using UnityEngine.Events; // 이벤트를 사용할거에요 -> UnityEngine.Events를
|
||||
|
||||
/// <summary>
|
||||
/// 보스 카운터 시스템 메인 컨트롤러.
|
||||
/// (수정됨: Config나 Player가 없어도 에러가 나지 않도록 안전장치가 추가된 버전)
|
||||
/// </summary>
|
||||
public class BossCounterSystem : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossCounterSystem을
|
||||
public class BossCounterSystem : MonoBehaviour // 클래스를 선언할거에요 -> 보스 카운터 시스템을
|
||||
{
|
||||
[Header("참조")] // 인스펙터 창에 제목을 표시할거에요 -> 참조 를
|
||||
[SerializeField] private BossCounterConfig config; // 변수를 선언할거에요 -> 카운터 설정 파일(BossCounterConfig)을 담을 config를
|
||||
[Header("설정")] // 인스펙터 제목을 달거에요 -> 설정을
|
||||
[SerializeField] private BossCounterConfig config; // 변수를 선언할거에요 -> 설정 파일 에셋을
|
||||
[SerializeField] private bool usePersistence = true; // 변수를 선언할거에요 -> 저장 사용 여부를
|
||||
|
||||
[Header("이벤트 (연출/UI 연동)")] // 인스펙터 창에 제목을 표시할거에요 -> 이벤트 (연출/UI 연동) 을
|
||||
[Tooltip("카운터 모드가 활성화될 때 발생 (CounterType 전달)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
public UnityEvent<CounterType> OnCounterActivated; // 이벤트를 선언할거에요 -> 카운터가 켜질 때 알릴 OnCounterActivated를
|
||||
private Dictionary<CounterType, int> _counters = new Dictionary<CounterType, int>(); // 변수를 선언할거에요 -> 내부 카운터 데이터를
|
||||
private PatternSelector _selector; // 변수를 선언할거에요 -> 패턴 선택 헬퍼를
|
||||
private BossCounterPersistence _persistence; // 변수를 선언할거에요 -> 저장소 헬퍼를
|
||||
|
||||
[Tooltip("카운터 모드가 비활성화될 때 발생")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
public UnityEvent<CounterType> OnCounterDeactivated; // 이벤트를 선언할거에요 -> 카운터가 꺼질 때 알릴 OnCounterDeactivated를
|
||||
// ⭐ [핵심] string을 매개변수로 받는 이벤트로 통일 (Feedback 스크립트 에러 해결용)
|
||||
public UnityEvent<CounterType, int> OnCounterUpdated; // 이벤트를 선언할거에요 -> 수치 변경 알림을
|
||||
public UnityEvent<string> OnCounterActivated; // 이벤트를 선언할거에요 -> 활성화 알림을 (string)
|
||||
public UnityEvent<string> OnCounterDeactivated; // 이벤트를 선언할거에요 -> 비활성화 알림을 (string)
|
||||
public UnityEvent<string> OnHabitChangeRewarded; // 이벤트를 선언할거에요 -> 보상 알림을 (string)
|
||||
|
||||
[Tooltip("보스가 카운터 패턴을 선택했을 때 발생 (패턴 이름 전달)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
public UnityEvent<string> OnCounterPatternSelected; // 이벤트를 선언할거에요 -> 패턴이 선택됐을 때 알릴 OnCounterPatternSelected를
|
||||
|
||||
[Tooltip("플레이어가 습관을 바꿔서 보상받을 때 발생")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
public UnityEvent OnHabitChangeRewarded; // 이벤트를 선언할거에요 -> 습관 변경 보상을 알릴 OnHabitChangeRewarded를
|
||||
|
||||
// ── 카운터 모드 상태 ──
|
||||
private Dictionary<CounterType, bool> activeCounters = new Dictionary<CounterType, bool>() // 변수를 선언하고 초기화할거에요 -> 각 카운터 타입의 활성 상태를 저장할 딕셔너리 activeCounters를
|
||||
private void Awake() // 함수를 실행할거에요 -> 초기화 Awake를
|
||||
{
|
||||
{ CounterType.Dodge, false }, // 값을 넣을거에요 -> 회피 카운터 초기값을 거짓(false)으로
|
||||
{ CounterType.Aim, false }, // 값을 넣을거에요 -> 조준 카운터 초기값을 거짓(false)으로
|
||||
{ CounterType.Pierce, false } // 값을 넣을거에요 -> 관통 카운터 초기값을 거짓(false)으로
|
||||
};
|
||||
_selector = new PatternSelector(config); // 생성할거에요 -> 패턴 선택기를
|
||||
_persistence = GetComponent<BossCounterPersistence>(); // 가져올거에요 -> 저장소 컴포넌트를
|
||||
if (!_persistence) _persistence = gameObject.AddComponent<BossCounterPersistence>(); // 없으면 추가할거에요 -> 저장소를
|
||||
|
||||
// ── Decay 타이머 ──
|
||||
private Dictionary<CounterType, float> counterTimers = new Dictionary<CounterType, float>() // 변수를 선언하고 초기화할거에요 -> 각 카운터의 남은 지속 시간을 저장할 딕셔너리 counterTimers를
|
||||
{
|
||||
{ CounterType.Dodge, 0f }, // 값을 넣을거에요 -> 회피 카운터 타이머를 0으로
|
||||
{ CounterType.Aim, 0f }, // 값을 넣을거에요 -> 조준 카운터 타이머를 0으로
|
||||
{ CounterType.Pierce, 0f } // 값을 넣을거에요 -> 관통 카운터 타이머를 0으로
|
||||
};
|
||||
|
||||
// ── 쿨타임 ──
|
||||
private float lastCounterPatternTime = -100f; // 변수를 선언할거에요 -> 마지막 카운터 패턴 사용 시간을 lastCounterPatternTime에 (초기값 -100)
|
||||
|
||||
// ── 습관 변경 보상 추적 ──
|
||||
private HashSet<CounterType> previousRunCounters = new HashSet<CounterType>(); // 변수를 선언할거에요 -> 지난 런에서 당했던 카운터 목록을 previousRunCounters에
|
||||
private HashSet<CounterType> currentRunActivatedCounters = new HashSet<CounterType>(); // 변수를 선언할거에요 -> 이번 런에서 발동된 카운터 목록을 currentRunActivatedCounters에
|
||||
private bool habitChangeRewardGranted = false; // 변수를 선언할거에요 -> 보상이 이미 지급되었는지 여부를 habitChangeRewardGranted에
|
||||
|
||||
// ── 보스 패턴 가중치 정의 ──
|
||||
[System.Serializable] // 직렬화할거에요 -> 인스펙터에서 수정할 수 있게 BossPattern 클래스를
|
||||
public class BossPattern // 내부 클래스를 선언할거에요 -> 보스 패턴 정보를 담을 BossPattern을
|
||||
{
|
||||
public string patternName; // 변수를 선언할거에요 -> 패턴 이름을 저장할 patternName을
|
||||
public float baseWeight = 1f; // 변수를 선언할거에요 -> 기본 가중치(확률)를 baseWeight에
|
||||
[Tooltip("이 패턴이 카운터 패턴인 경우 해당 타입")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
public CounterType counterType = CounterType.None; // 변수를 선언할거에요 -> 이 패턴이 어떤 카운터 타입인지 지정할 counterType을
|
||||
[Tooltip("카운터 발동 시 보조적으로 가중치가 올라가는 타입")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
public CounterType subCounterType = CounterType.None; // 변수를 선언할거에요 -> 보너스 가중치를 받을 보조 카운터 타입 subCounterType을
|
||||
foreach (CounterType t in System.Enum.GetValues(typeof(CounterType))) _counters[t] = 0; // 초기화할거에요 -> 모든 카운터를 0으로
|
||||
}
|
||||
|
||||
[Header("보스 패턴 목록")] // 인스펙터 창에 제목을 표시할거에요 -> 보스 패턴 목록 을
|
||||
[SerializeField] private List<BossPattern> bossPatterns = new List<BossPattern>(); // 리스트를 선언할거에요 -> 보스의 모든 패턴 정보를 담을 bossPatterns를
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 초기화
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
private void Start() // 함수를 실행할거에요 -> 게임 시작 시 호출되는 Start를
|
||||
private void Start() // 함수를 실행할거에요 -> 시작 Start를
|
||||
{
|
||||
// 🚨 [안전장치] 설정 파일이 없으면 경고 출력
|
||||
if (config == null) // 조건이 맞으면 실행할거에요 -> 설정 파일(config)이 연결되지 않았다면
|
||||
{
|
||||
Debug.LogWarning("⚠ [BossCounterSystem] Config(설정 파일)가 없습니다! AI가 정상 작동하지 않을 수 있습니다."); // 경고 로그를 출력할거에요 -> 설정 파일 누락 경고를
|
||||
}
|
||||
if (usePersistence && _persistence) _persistence.LoadData(_counters); // 불러올거에요 -> 저장된 데이터를
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 보스 전투 시작 시 호출. 이전 런에서 발동된 카운터 정보를 기반으로 초기화.
|
||||
/// </summary>
|
||||
public void InitializeBattle() // 함수를 선언할거에요 -> 전투 시작 시 초기화를 담당할 InitializeBattle을
|
||||
public void InitializeBattle() { } // 함수를 비워둘거에요 -> 외부 호출 호환성을 위해
|
||||
|
||||
public void RegisterPlayerAction(CounterType type) // 함수를 선언할거에요 -> 플레이어 행동 기록을
|
||||
{
|
||||
// 이전 런에서 잠금 해제된 카운터들을 기록 (습관 변경 보상 판단용)
|
||||
previousRunCounters.Clear(); // 비울거에요 -> 이전 런 카운터 목록을
|
||||
currentRunActivatedCounters.Clear(); // 비울거에요 -> 이번 런 카운터 목록을
|
||||
habitChangeRewardGranted = false; // 초기화할거에요 -> 보상 지급 여부를 거짓(false)으로
|
||||
if (!_counters.ContainsKey(type)) _counters[type] = 0; // 없으면 만들거에요 -> 키를
|
||||
_counters[type]++; // 증가시킬거에요 -> 카운트 값을
|
||||
|
||||
var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소 인스턴스를 persistence에
|
||||
if (persistence != null) // 조건이 맞으면 실행할거에요 -> 저장소가 존재한다면
|
||||
{
|
||||
if (persistence.Data.dodgeCounterActivations > 0 && persistence.IsUnlocked(CounterType.Dodge)) // 조건이 맞으면 실행할거에요 -> 회피 카운터 기록이 있고 해제되었다면
|
||||
previousRunCounters.Add(CounterType.Dodge); // 추가할거에요 -> 회피 타입을 이전 런 목록에
|
||||
if (persistence.Data.aimCounterActivations > 0 && persistence.IsUnlocked(CounterType.Aim)) // 조건이 맞으면 실행할거에요 -> 조준 카운터 기록이 있고 해제되었다면
|
||||
previousRunCounters.Add(CounterType.Aim); // 추가할거에요 -> 조준 타입을 이전 런 목록에
|
||||
if (persistence.Data.pierceCounterActivations > 0 && persistence.IsUnlocked(CounterType.Pierce)) // 조건이 맞으면 실행할거에요 -> 관통 카운터 기록이 있고 해제되었다면
|
||||
previousRunCounters.Add(CounterType.Pierce); // 추가할거에요 -> 관통 타입을 이전 런 목록에
|
||||
}
|
||||
|
||||
// 모든 카운터 모드 OFF
|
||||
foreach (var type in new[] { CounterType.Dodge, CounterType.Aim, CounterType.Pierce }) // 반복할거에요 -> 모든 카운터 타입(회피, 조준, 관통)에 대해
|
||||
{
|
||||
activeCounters[type] = false; // 상태를 바꿀거에요 -> 해당 카운터를 비활성화(false)로
|
||||
counterTimers[type] = 0f; // 값을 바꿀거에요 -> 해당 카운터 타이머를 0으로
|
||||
}
|
||||
|
||||
lastCounterPatternTime = -100f; // 값을 초기화할거에요 -> 마지막 패턴 사용 시간을 -100으로
|
||||
|
||||
Debug.Log($"[BossCounter] 전투 시작. 이전 런 카운터: [{string.Join(", ", previousRunCounters)}]"); // 로그를 출력할거에요 -> 전투 시작 알림과 이전 런 정보를
|
||||
OnCounterUpdated?.Invoke(type, _counters[type]); // 알릴거에요 -> 수치 변경 이벤트를
|
||||
OnCounterActivated?.Invoke(type.ToString()); // 알릴거에요 -> 활성화 이벤트를 문자열로 변환해서
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 매 프레임 업데이트
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를
|
||||
public string SelectBossPattern() // 함수를 선언할거에요 -> 패턴 선택을
|
||||
{
|
||||
// 🚨 [안전장치] 플레이어 트래커나 설정 파일이 없으면 실행 중지 (NullReferenceException 방지)
|
||||
if (PlayerBehaviorTracker.Instance == null || config == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어 추적기나 설정 파일이 없다면
|
||||
string pattern = _selector.SelectPattern(_counters); // 선택할거에요 -> 헬퍼를 통해 패턴을
|
||||
|
||||
EvaluateCounters(); // 함수를 실행할거에요 -> 카운터 발동 조건을 검사하는 EvaluateCounters를
|
||||
DecayCounters(); // 함수를 실행할거에요 -> 카운터 지속 시간을 관리하는 DecayCounters를
|
||||
CheckHabitChangeReward(); // 함수를 실행할거에요 -> 습관 변경 보상을 체크하는 CheckHabitChangeReward를
|
||||
var keys = new List<CounterType>(_counters.Keys); // 복사할거에요 -> 키 목록을
|
||||
foreach (var k in keys) if (_counters[k] > 0) _counters[k]--; // 줄일거에요 -> 쿨다운을 위해 값을
|
||||
|
||||
if (usePersistence && _persistence) _persistence.SaveData(_counters); // 저장할거에요 -> 갱신된 데이터를
|
||||
return pattern; // 반환할거에요 -> 선택된 패턴 이름을
|
||||
}
|
||||
|
||||
/// <summary>플레이어 행동 데이터를 읽고 카운터 모드 ON/OFF 판단</summary>
|
||||
private void EvaluateCounters() // 함수를 선언할거에요 -> 카운터 발동 여부를 판단하는 EvaluateCounters를
|
||||
public Dictionary<CounterType, int> GetActiveCounters() => new Dictionary<CounterType, int>(_counters); // 반환할거에요 -> 데이터 복사본을
|
||||
|
||||
// ⭐ [복구] 보상 여부 확인 함수
|
||||
public bool IsHabitChangeRewarded() // 함수를 선언할거에요 -> 보상 획득 여부 확인을
|
||||
{
|
||||
// 🚨 [안전장치] Config가 없으면 계산 불가
|
||||
if (config == null) return; // 조건이 맞으면 중단할거에요 -> 설정 파일이 없다면
|
||||
|
||||
var tracker = PlayerBehaviorTracker.Instance; // 변수를 가져올거에요 -> 플레이어 행동 추적기를 tracker에
|
||||
var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소를 persistence에
|
||||
|
||||
// ── 회피 카운터 ──
|
||||
bool dodgeUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Dodge); // 값을 확인할거에요 -> 회피 카운터가 해금되었는지 여부를
|
||||
int dodgeThreshold = config.GetEffectiveDodgeThreshold(dodgeUnlocked); // 값을 가져올거에요 -> 현재 적용할 회피 임계값을
|
||||
|
||||
if (tracker.DodgeCount >= dodgeThreshold) // 조건이 맞으면 실행할거에요 -> 플레이어 회피 횟수가 임계값 이상이라면
|
||||
{
|
||||
// 잠금 해제 안 된 상태면 첫 발동 시 잠금 해제 (첫 런에서도 발동 가능)
|
||||
// 잠금 해제된 상태면 더 낮은 임계치로 발동
|
||||
ActivateCounter(CounterType.Dodge); // 함수를 실행할거에요 -> 회피 카운터를 발동시키는 ActivateCounter를
|
||||
}
|
||||
|
||||
// ── 조준 카운터 ──
|
||||
bool aimUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Aim); // 값을 확인할거에요 -> 조준 카운터가 해금되었는지 여부를
|
||||
float aimThreshold = config.GetEffectiveAimThreshold(aimUnlocked); // 값을 가져올거에요 -> 현재 적용할 조준 임계값을
|
||||
|
||||
if (tracker.AimHoldTime >= aimThreshold) // 조건이 맞으면 실행할거에요 -> 플레이어 조준 시간이 임계값 이상이라면
|
||||
{
|
||||
ActivateCounter(CounterType.Aim); // 함수를 실행할거에요 -> 조준 카운터를 발동시키는 ActivateCounter를
|
||||
}
|
||||
|
||||
// ── 관통 카운터 ──
|
||||
bool pierceUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Pierce); // 값을 확인할거에요 -> 관통 카운터가 해금되었는지 여부를
|
||||
float pierceThreshold = config.GetEffectivePierceThreshold(pierceUnlocked); // 값을 가져올거에요 -> 현재 적용할 관통 임계값을
|
||||
|
||||
if (tracker.TotalShotsInWindow >= config.minShotsForPierceCheck // 조건이 맞으면 실행할거에요 -> 최소 발사 횟수를 충족하고
|
||||
&& tracker.PierceRatio >= pierceThreshold) // 조건이 맞으면 실행할거에요 -> 관통 공격 비율이 임계값 이상이라면
|
||||
{
|
||||
ActivateCounter(CounterType.Pierce); // 함수를 실행할거에요 -> 관통 카운터를 발동시키는 ActivateCounter를
|
||||
}
|
||||
return false; // 임시 반환할거에요 -> 로직 구현 전까지 거짓(false)을
|
||||
}
|
||||
|
||||
private void ActivateCounter(CounterType type) // 함수를 선언할거에요 -> 특정 카운터를 활성화하는 ActivateCounter를
|
||||
{
|
||||
// 🚨 [안전장치] Config 확인
|
||||
if (config == null) return; // 조건이 맞으면 중단할거에요 -> 설정 파일이 없다면
|
||||
|
||||
counterTimers[type] = config.counterDecayTime; // 값을 설정할거에요 -> 해당 카운터의 지속 시간을 설정값으로
|
||||
|
||||
if (!activeCounters[type]) // 조건이 맞으면 실행할거에요 -> 해당 카운터가 현재 꺼져 있다면
|
||||
{
|
||||
activeCounters[type] = true; // 상태를 바꿀거에요 -> 카운터 활성 상태를 참(true)으로
|
||||
currentRunActivatedCounters.Add(type); // 추가할거에요 -> 이번 런 발동 목록에 해당 타입을
|
||||
|
||||
// 영구 잠금 해제
|
||||
var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소를 persistence에
|
||||
if (persistence != null) // 조건이 맞으면 실행할거에요 -> 저장소가 있다면
|
||||
{
|
||||
persistence.UnlockCounter(type); // 함수를 실행할거에요 -> 해당 카운터를 영구 해금하는 UnlockCounter를
|
||||
persistence.RecordActivation(type); // 함수를 실행할거에요 -> 발동 횟수를 기록하는 RecordActivation을
|
||||
}
|
||||
|
||||
OnCounterActivated?.Invoke(type); // 이벤트를 실행할거에요 -> 카운터 활성화 알림 이벤트를
|
||||
Debug.Log($"[BossCounter] {type} 카운터 활성화!"); // 로그를 출력할거에요 -> 카운터 활성화 메시지를
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>시간이 지나면 카운터 모드 자동 해제</summary>
|
||||
private void DecayCounters() // 함수를 선언할거에요 -> 시간이 지나면 카운터를 끄는 DecayCounters를
|
||||
{
|
||||
foreach (var type in new[] { CounterType.Dodge, CounterType.Aim, CounterType.Pierce }) // 반복할거에요 -> 모든 카운터 타입에 대해
|
||||
{
|
||||
if (!activeCounters[type]) continue; // 조건이 맞으면 건너뛸거에요 -> 이미 꺼져 있는 카운터라면
|
||||
|
||||
counterTimers[type] -= Time.deltaTime; // 값을 뺄거에요 -> 남은 시간에서 프레임 시간을
|
||||
if (counterTimers[type] <= 0f) // 조건이 맞으면 실행할거에요 -> 남은 시간이 0 이하라면
|
||||
{
|
||||
activeCounters[type] = false; // 상태를 바꿀거에요 -> 카운터 활성 상태를 거짓(false)으로
|
||||
OnCounterDeactivated?.Invoke(type); // 이벤트를 실행할거에요 -> 카운터 비활성화 알림 이벤트를
|
||||
Debug.Log($"[BossCounter] {type} 카운터 Decay로 비활성화"); // 로그를 출력할거에요 -> 시간 만료로 인한 비활성화 메시지를
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 습관 변경 보상
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// 이전 런에서 카운터 당한 습관을 이번 런에서 보이지 않으면 보상.
|
||||
/// 전투 중반(30초 이후) 한 번 체크.
|
||||
/// </summary>
|
||||
private float battleTimer = 0f; // 변수를 초기화할거에요 -> 전투 진행 시간을 0으로
|
||||
private const float HABIT_CHECK_TIME = 30f; // 상수를 정의할거에요 -> 습관 체크 시점을 30초로
|
||||
|
||||
private void CheckHabitChangeReward() // 함수를 선언할거에요 -> 습관 변경 보상을 확인하는 CheckHabitChangeReward를
|
||||
{
|
||||
if (habitChangeRewardGranted || previousRunCounters.Count == 0) return; // 조건이 맞으면 중단할거에요 -> 이미 보상을 받았거나 이전 런 기록이 없다면
|
||||
|
||||
battleTimer += Time.deltaTime; // 값을 더할거에요 -> 전투 진행 시간에 프레임 시간을
|
||||
if (battleTimer < HABIT_CHECK_TIME) return; // 조건이 맞으면 중단할거에요 -> 아직 30초가 안 지났다면
|
||||
|
||||
// 이전 런에서 발동된 카운터 중, 이번 런에서 아직 발동 안 된 것이 있으면 보상
|
||||
foreach (var prevCounter in previousRunCounters) // 반복할거에요 -> 이전 런 카운터 목록에 대해
|
||||
{
|
||||
if (!currentRunActivatedCounters.Contains(prevCounter)) // 조건이 맞으면 실행할거에요 -> 이번 런 목록에 해당 카운터가 없다면 (습관 고침)
|
||||
{
|
||||
habitChangeRewardGranted = true; // 상태를 바꿀거에요 -> 보상 지급 여부를 참(true)으로
|
||||
OnHabitChangeRewarded?.Invoke(); // 이벤트를 실행할거에요 -> 보상 지급 알림 이벤트를
|
||||
Debug.Log($"[BossCounter] 플레이어가 {prevCounter} 습관을 바꿈! 보상 부여"); // 로그를 출력할거에요 -> 보상 부여 메시지를
|
||||
break; // 중단할거에요 -> 보상은 한 번만 주므로 반복문을
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 패턴 선택 (보스 AI에서 호출)
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// 현재 카운터 상태를 반영하여 보스 패턴을 가중치 랜덤으로 선택합니다.
|
||||
/// 보스 AI의 패턴 선택 시점에 호출하세요.
|
||||
/// </summary>
|
||||
/// <returns>선택된 패턴 이름</returns>
|
||||
public string SelectBossPattern() // 함수를 선언할거에요 -> 보스 패턴을 선택해서 반환하는 SelectBossPattern을
|
||||
{
|
||||
if (bossPatterns.Count == 0) // 조건이 맞으면 실행할거에요 -> 등록된 패턴이 하나도 없다면
|
||||
{
|
||||
Debug.LogWarning("[BossCounter] 보스 패턴이 등록되지 않았습니다!"); // 경고 로그를 출력할거에요 -> 패턴 없음 경고를
|
||||
return "Normal"; // 값을 반환할거에요 -> 기본값 "Normal"을
|
||||
}
|
||||
|
||||
// 🚨 [안전장치] Config가 없으면 그냥 랜덤 패턴 반환 (멈춤 방지)
|
||||
if (config == null) // 조건이 맞으면 실행할거에요 -> 설정 파일이 없다면
|
||||
{
|
||||
int randomIndex = Random.Range(0, bossPatterns.Count); // 값을 랜덤으로 뽑을거에요 -> 0부터 패턴 개수 사이의 인덱스를
|
||||
return bossPatterns[randomIndex].patternName; // 값을 반환할거에요 -> 랜덤하게 뽑힌 패턴 이름을
|
||||
}
|
||||
|
||||
// 가중치 계산
|
||||
float[] weights = new float[bossPatterns.Count]; // 배열을 만들거에요 -> 각 패턴의 가중치를 담을 weights 배열을
|
||||
float totalWeight = 0f; // 변수를 초기화할거에요 -> 전체 가중치 합계를 0으로
|
||||
float counterWeight = 0f; // 변수를 초기화할거에요 -> 카운터 패턴들의 가중치 합계를 0으로
|
||||
|
||||
for (int i = 0; i < bossPatterns.Count; i++) // 반복할거에요 -> 모든 패턴에 대해
|
||||
{
|
||||
weights[i] = bossPatterns[i].baseWeight; // 값을 넣을거에요 -> 기본 가중치를 배열에
|
||||
|
||||
// 카운터 패턴 가중치 증가
|
||||
if (bossPatterns[i].counterType != CounterType.None // 조건이 맞으면 실행할거에요 -> 카운터 타입이 설정되어 있고
|
||||
&& activeCounters.ContainsKey(bossPatterns[i].counterType) // 조건이 맞으면 실행할거에요 -> 딕셔너리에 키가 존재하며
|
||||
&& activeCounters[bossPatterns[i].counterType]) // 조건이 맞으면 실행할거에요 -> 해당 카운터가 활성 상태라면
|
||||
{
|
||||
// 쿨타임 체크
|
||||
if (Time.time - lastCounterPatternTime >= config.counterCooldown) // 조건이 맞으면 실행할거에요 -> 마지막 사용 후 쿨타임이 지났다면
|
||||
{
|
||||
weights[i] += config.counterWeightBonus; // 값을 더할거에요 -> 카운터 보너스 가중치를
|
||||
}
|
||||
}
|
||||
|
||||
// 보조 카운터 가중치
|
||||
if (bossPatterns[i].subCounterType != CounterType.None // 조건이 맞으면 실행할거에요 -> 보조 카운터 타입이 설정되어 있고
|
||||
&& activeCounters.ContainsKey(bossPatterns[i].subCounterType) // 조건이 맞으면 실행할거에요 -> 딕셔너리에 키가 존재하며
|
||||
&& activeCounters[bossPatterns[i].subCounterType]) // 조건이 맞으면 실행할거에요 -> 해당 보조 카운터가 활성 상태라면
|
||||
{
|
||||
weights[i] += config.counterSubWeightBonus; // 값을 더할거에요 -> 보조 카운터 보너스 가중치를
|
||||
}
|
||||
|
||||
// 습관 변경 보상: 일반 패턴 가중치 감소 (= 난이도 완화)
|
||||
if (habitChangeRewardGranted && bossPatterns[i].counterType == CounterType.None) // 조건이 맞으면 실행할거에요 -> 보상을 받았고 일반 패턴이라면
|
||||
{
|
||||
weights[i] *= (1f - config.habitChangeRewardRatio); // 값을 곱할거에요 -> 가중치를 감소 비율만큼 줄여서
|
||||
}
|
||||
|
||||
totalWeight += weights[i]; // 값을 더할거에요 -> 전체 가중치 합계에 현재 가중치를
|
||||
|
||||
if (bossPatterns[i].counterType != CounterType.None) // 조건이 맞으면 실행할거에요 -> 카운터 패턴이라면
|
||||
counterWeight += weights[i]; // 값을 더할거에요 -> 카운터 가중치 합계에
|
||||
}
|
||||
|
||||
// 빈도 상한 체크: 카운터 패턴 총 비율이 maxCounterFrequency를 넘지 않도록
|
||||
if (totalWeight > 0f && counterWeight / totalWeight > config.maxCounterFrequency) // 조건이 맞으면 실행할거에요 -> 카운터 패턴 비율이 최대 허용치를 초과한다면
|
||||
{
|
||||
float allowedCounterWeight = (totalWeight - counterWeight) * config.maxCounterFrequency / (1f - config.maxCounterFrequency); // 값을 계산할거에요 -> 허용 가능한 카운터 총 가중치를
|
||||
float scale = allowedCounterWeight / counterWeight; // 값을 계산할거에요 -> 가중치를 줄일 비율(scale)을
|
||||
|
||||
for (int i = 0; i < bossPatterns.Count; i++) // 반복할거에요 -> 모든 패턴에 대해
|
||||
{
|
||||
if (bossPatterns[i].counterType != CounterType.None) // 조건이 맞으면 실행할거에요 -> 카운터 패턴이라면
|
||||
{
|
||||
float bonus = weights[i] - bossPatterns[i].baseWeight; // 값을 계산할거에요 -> 추가된 보너스 가중치를
|
||||
weights[i] = bossPatterns[i].baseWeight + bonus * scale; // 값을 수정할거에요 -> 보너스를 비율대로 줄여서 다시 적용
|
||||
}
|
||||
}
|
||||
|
||||
// 총 가중치 재계산
|
||||
totalWeight = 0f; // 값을 초기화할거에요 -> 전체 합계를 0으로
|
||||
for (int i = 0; i < weights.Length; i++) // 반복할거에요 -> 모든 가중치에 대해
|
||||
totalWeight += weights[i]; // 값을 더할거에요 -> 전체 합계에
|
||||
}
|
||||
|
||||
// 가중치 랜덤 선택
|
||||
float roll = Random.Range(0f, totalWeight); // 값을 랜덤으로 뽑을거에요 -> 0부터 전체 가중치 사이의 값을 roll에
|
||||
float cumulative = 0f; // 변수를 초기화할거에요 -> 누적 가중치를 0으로
|
||||
|
||||
for (int i = 0; i < bossPatterns.Count; i++) // 반복할거에요 -> 모든 패턴에 대해
|
||||
{
|
||||
cumulative += weights[i]; // 값을 더할거에요 -> 누적 가중치에 현재 가중치를
|
||||
if (roll <= cumulative) // 조건이 맞으면 실행할거에요 -> 랜덤값이 누적치보다 작거나 같다면 (당첨)
|
||||
{
|
||||
string selected = bossPatterns[i].patternName; // 값을 저장할거에요 -> 선택된 패턴 이름을 selected에
|
||||
|
||||
// 카운터 패턴이면 쿨타임 기록
|
||||
if (bossPatterns[i].counterType != CounterType.None) // 조건이 맞으면 실행할거에요 -> 카운터 패턴이라면
|
||||
{
|
||||
lastCounterPatternTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 사용 시간으로
|
||||
}
|
||||
|
||||
OnCounterPatternSelected?.Invoke(selected); // 이벤트를 실행할거에요 -> 패턴 선택 알림 이벤트를
|
||||
return selected; // 값을 반환할거에요 -> 선택된 패턴 이름을
|
||||
}
|
||||
}
|
||||
|
||||
return bossPatterns[bossPatterns.Count - 1].patternName; // 값을 반환할거에요 -> 만약 선택되지 않았다면 마지막 패턴을 (안전장치)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 외부 조회 (여기가 아까 빠졌던 부분입니다!)
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>특정 카운터 모드가 현재 활성 상태인지 확인</summary>
|
||||
public bool IsCounterActive(CounterType type) // 함수를 선언할거에요 -> 특정 카운터 활성 여부를 반환하는 IsCounterActive를
|
||||
{
|
||||
return activeCounters.ContainsKey(type) && activeCounters[type]; // 값을 반환할거에요 -> 딕셔너리에 키가 있고 값이 참(true)인지를
|
||||
}
|
||||
|
||||
/// <summary>현재 활성화된 모든 카운터 타입 반환</summary>
|
||||
public List<CounterType> GetActiveCounters() // 함수를 선언할거에요 -> 활성화된 모든 카운터 목록을 반환하는 GetActiveCounters를
|
||||
{
|
||||
var result = new List<CounterType>(); // 리스트를 만들거에요 -> 결과를 담을 result 리스트를
|
||||
foreach (var kvp in activeCounters) // 반복할거에요 -> 모든 카운터 상태에 대해
|
||||
{
|
||||
if (kvp.Value) result.Add(kvp.Key); // 조건이 맞으면 실행할거에요 -> 활성화(true) 상태라면 리스트에 추가하기를
|
||||
}
|
||||
return result; // 값을 반환할거에요 -> 완성된 리스트를
|
||||
}
|
||||
|
||||
/// <summary>습관 변경 보상이 활성화되었는지 확인</summary>
|
||||
public bool IsHabitChangeRewarded => habitChangeRewardGranted; // 프로퍼티를 선언할거에요 -> 보상 지급 여부를 외부에서 읽을 수 있는 IsHabitChangeRewarded를
|
||||
}
|
||||
|
|
@ -1,443 +1,560 @@
|
|||
using UnityEngine; // 유니티 기본 기능을 불러올거에요 -> UnityEngine을
|
||||
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
||||
using System.Collections.Generic; // Dictionary를 사용할거에요 -> System.Collections.Generic을
|
||||
|
||||
public class NorcielBoss : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 NorcielBoss를
|
||||
{
|
||||
[Header("--- 🧠 두뇌 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🧠 두뇌 연결 ---을
|
||||
[SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 보스의 패턴을 결정할 counterSystem을
|
||||
{ // 코드 블록을 시작할거에요 -> NorcielBoss 범위를
|
||||
|
||||
[Header("--- ⚔️ 패턴 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- ⚔️ 패턴 설정 ---을
|
||||
[SerializeField] private float patternInterval = 3f; // 변수를 선언할거에요 -> 공격 후 다음 공격까지 대기 시간을 3초로 지정하는 patternInterval을
|
||||
[SerializeField] private float attackRange = 3f; // 변수를 선언할거에요 -> 이 거리 안에 플레이어가 들어오면 공격하는 attackRange를 3으로
|
||||
[Header("--- 🧠 두뇌 연결 ---")] // 인스펙터 제목을 달거에요 -> 두뇌 연결 설정을
|
||||
[SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 패턴 결정을 위한 카운터 시스템을
|
||||
|
||||
[Header("--- 🎱 무기(쇠공) 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🎱 무기 설정 ---을
|
||||
[SerializeField] private GameObject ironBall; // 변수를 선언할거에요 -> 던지고 주울 무기 오브젝트인 ironBall을
|
||||
[SerializeField] private Transform handHolder; // 변수를 선언할거에요 -> 무기를 잡을 손의 위치인 handHolder를
|
||||
[Header("--- ⚔️ 패턴 설정 ---")] // 인스펙터 제목을 달거에요 -> 패턴 설정을
|
||||
[SerializeField] private float patternInterval = 3f; // 변수를 선언할거에요 -> 공격 후 다음 공격까지 딜레이 시간을
|
||||
[SerializeField] private float attackRange = 3f; // 변수를 선언할거에요 -> 공격이 가능한 사거리를
|
||||
|
||||
[Header("--- 📊 UI 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 📊 UI 연결 ---을
|
||||
[SerializeField] private GameObject bossHealthBar; // 변수를 선언할거에요 -> 보스의 체력 UI를 담을 bossHealthBar를
|
||||
[Header("--- 🎱 무기(쇠공) 설정 ---")] // 인스펙터 제목을 달거에요 -> 무기 설정을
|
||||
[SerializeField] private GameObject ironBall; // 변수를 선언할거에요 -> 보스가 던질 쇠공 오브젝트를
|
||||
[SerializeField] private Transform handHolder; // 변수를 선언할거에요 -> 쇠공을 쥘 손의 위치를
|
||||
|
||||
// ⭐ [핵심 수정] 애니메이션 이름을 인스펙터에서 맞출 수 있게 변수로 분리
|
||||
[Header("--- 🎬 애니메이션 이름 설정 (Animator와 일치시킬 것!) ---")]
|
||||
[Header("--- 📊 UI 연결 ---")] // 인스펙터 제목을 달거에요 -> UI 설정을
|
||||
[SerializeField] private GameObject bossHealthBar; // 변수를 선언할거에요 -> 화면에 보일 보스 체력바를
|
||||
|
||||
[Header("--- 🎬 애니메이션 이름 설정 (Animator와 일치시킬 것!) ---")] // 인스펙터 제목을 달거에요 -> 애니메이션 이름 설정을
|
||||
[SerializeField] private string anim_Roar = "Roar"; // 변수를 선언할거에요 -> 포효 애니메이션 이름을
|
||||
[SerializeField] private string anim_Idle = "Monster_Idle"; // 변수를 선언할거에요 -> 대기 애니메이션 이름을
|
||||
[SerializeField] private string anim_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을
|
||||
[SerializeField] private string anim_Pickup = "Skill_Pickup"; // 변수를 선언할거에요 -> 무기 줍기 애니메이션 이름을
|
||||
[SerializeField] private string anim_Pickup = "Skill_Pickup"; // 변수를 선언할거에요 -> 줍기 애니메이션 이름을
|
||||
[SerializeField] private string anim_Throw = "Attack_Throw"; // 변수를 선언할거에요 -> 던지기 애니메이션 이름을
|
||||
|
||||
[Header("--- 🎬 스킬 애니메이션 이름 ---")]
|
||||
[SerializeField] private string anim_DashReady = "Skill_Dash_Ready"; // 변수를 선언할거에요 -> 돌진 준비 애니메이션 이름을
|
||||
[SerializeField] private string anim_DashGo = "Skill_Dash_Go"; // 변수를 선언할거에요 -> 돌진 출발 애니메이션 이름을
|
||||
[SerializeField] private string anim_SmashCharge = "Skill_Smash_Charge"; // 변수를 선언할거에요 -> 내려찍기 기모으기 이름을
|
||||
[SerializeField] private string anim_SmashImpact = "Skill_Smash_Impact"; // 변수를 선언할거에요 -> 내려찍기 타격 이름을
|
||||
[SerializeField] private string anim_Sweep = "Skill_Sweep"; // 변수를 선언할거에요 -> 휩쓸기 애니메이션 이름을
|
||||
|
||||
[Header("--- 🔍 디버그 (읽기 전용) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🔍 디버그 ---를
|
||||
[SerializeField] private string debugState = "대기 중"; // 변수를 선언할거에요 -> 현재 보스 행동을 문장으로 보여줄 debugState를
|
||||
|
||||
// --- 내부 변수들 ---
|
||||
private float _timer; // 변수를 선언할거에요 -> 공격 쿨타임을 계산할 내부 타이머 _timer를
|
||||
private Rigidbody rb; // 변수를 선언할거에요 -> 보스의 물리 엔진을 제어할 rb를
|
||||
private Rigidbody ballRb; // 변수를 선언할거에요 -> 쇠공의 물리 엔진을 제어할 ballRb를
|
||||
|
||||
private bool isBattleStarted = false; // 변수를 초기화할거에요 -> 전투 시작 상태를 거짓(false)으로
|
||||
private bool isWeaponless = false; // 변수를 초기화할거에요 -> 맨손 상태를 거짓(false)으로
|
||||
private bool isPerformingAction = false; // 변수를 초기화할거에요 -> 연출 진행 상태를 거짓(false)으로
|
||||
private Transform target; // 변수를 선언할거에요 -> 보스가 쫓아갈 대상의 위치인 target을
|
||||
|
||||
protected override void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 가장 먼저 호출되는 Awake를
|
||||
{
|
||||
base.Awake(); // 부모 클래스의 함수를 먼저 실행할거에요 -> MonsterClass의 Awake 로직을
|
||||
rb = GetComponent<Rigidbody>(); // 컴포넌트를 가져와서 저장할거에요 -> 보스 자신의 Rigidbody를 rb에
|
||||
if (ironBall != null) ballRb = ironBall.GetComponent<Rigidbody>(); // 조건이 맞으면 가져올거에요 -> ironBall이 있다면 그것의 Rigidbody를 ballRb에
|
||||
}
|
||||
|
||||
private void Start() // 함수를 실행할거에요 -> 첫 프레임이 시작될 때 호출되는 Start를
|
||||
{
|
||||
StartBossBattle(); // 함수를 실행할거에요 -> Roar 연출 후 전투를 시작하는 StartBossBattle을
|
||||
}
|
||||
|
||||
protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 몬스터 초기화를 담당하는 Init을
|
||||
{
|
||||
base.Init(); // 부모 초기화 호출
|
||||
_timer = 0f; // 값을 넣을거에요 -> 전투 시작하자마자 첫 공격이 가능하게 타이머를 0으로
|
||||
isBattleStarted = false; // 상태를 바꿀거에요 -> 전투 시작 상태를 거짓(false)으로
|
||||
isWeaponless = false; // 상태를 바꿀거에요 -> 맨손 상태를 거짓(false)으로
|
||||
isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 상태를 거짓(false)으로
|
||||
|
||||
IsAggroed = true; // 상태를 바꿀거에요 -> 부모 클래스의 영구 어그로 상태를 참(true)으로
|
||||
mobRenderer = null; // 값을 지울거에요 -> 화면 밖 최적화를 끄기 위해 렌더러를 null로
|
||||
optimizationDistance = 9999f; // 값을 바꿀거에요 -> 거리가 멀어져도 멈추지 않게 최적화 거리를 9999로
|
||||
|
||||
GameObject playerObj = GameObject.FindWithTag("Player"); // 오브젝트를 찾을거에요 -> Player 태그가 붙은 녀석을
|
||||
if (playerObj != null) // 조건이 맞으면 실행할거에요 -> 태그로 찾은 플레이어가 존재한다면
|
||||
{
|
||||
target = playerObj.transform; // 값을 넣을거에요 -> 플레이어의 위치를 target에
|
||||
playerTransform = playerObj.transform; // 값을 넣을거에요 -> 부모의 추적 변수에도 플레이어의 위치를
|
||||
}
|
||||
else // 조건이 틀리면 실행할거에요 -> 태그로 찾지 못했다면
|
||||
{
|
||||
var playerScript = FindObjectOfType<PlayerMovement>(); // 컴포넌트를 찾을거에요 -> PlayerMovement 스크립트를 가진 오브젝트를
|
||||
if (playerScript != null) // 조건이 맞으면 실행할거에요 -> 스크립트로 플레이어를 찾았다면
|
||||
{
|
||||
target = playerScript.transform; // 값을 넣을거에요 -> 플레이어의 위치를 target에
|
||||
playerTransform = playerScript.transform; // 값을 넣을거에요 -> 부모의 추적 변수에도 플레이어의 위치를
|
||||
}
|
||||
}
|
||||
|
||||
if (target == null) // 조건이 맞으면 실행할거에요 -> 어떤 방법으로도 플레이어를 못 찾았다면
|
||||
Debug.LogError("❌ [Boss] 플레이어를 찾지 못했습니다! Player 태그 또는 PlayerMovement 확인!"); // 에러를 출력할거에요 -> 플레이어 없음 경고 메시지를
|
||||
|
||||
currentHP = maxHP; // 체력을 채울거에요 -> 현재 체력을 최대 체력으로
|
||||
StartCoroutine(SafeInitNavMesh()); // 코루틴을 실행할거에요 -> 길찾기를 안전하게 켜는 SafeInitNavMesh를
|
||||
|
||||
if (bossHealthBar != null) bossHealthBar.SetActive(false); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에서 끄기를
|
||||
if (ballRb != null) ballRb.isKinematic = true; // 조건이 맞으면 실행할거에요 -> 쇠공 물리가 있다면 중력 연산을 끄기를
|
||||
if (animator != null) animator.Play(anim_Idle); // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 대기 모션을
|
||||
}
|
||||
|
||||
private IEnumerator SafeInitNavMesh() // 코루틴 함수를 선언할거에요 -> 길찾기를 안전하게 초기화하는 SafeInitNavMesh를
|
||||
{
|
||||
yield return null; // 기다릴거에요 -> 다음 프레임이 될 때까지 한 턴을
|
||||
if (agent != null) // 조건이 맞으면 실행할거에요 -> NavMeshAgent가 존재한다면
|
||||
{
|
||||
int retry = 0; // 변수를 선언할거에요 -> 재시도 횟수를 셀 retry를 0으로
|
||||
while (!agent.isOnNavMesh && retry < 5) // 반복할거에요 -> 바닥에 없고 시도가 5번 미만인 동안
|
||||
{
|
||||
retry++; // 값을 증가시킬거에요 -> 재시도 횟수를 1만큼
|
||||
yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초의 시간을
|
||||
}
|
||||
if (agent.isOnNavMesh) agent.isStopped = true; // 조건이 맞으면 실행할거에요 -> 바닥에 닿았다면 이동을 정지로
|
||||
agent.enabled = false; // 기능을 끌거에요 -> 전투 시작 전까지 NavMeshAgent를 비활성화로
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를
|
||||
{
|
||||
base.Update(); // 부모 클래스의 Update도 호출 (최적화 로직 등)
|
||||
|
||||
if (!isDead) // 조건이 맞으면 실행할거에요 -> 보스가 살아있다면
|
||||
{
|
||||
ExecuteAILogic(); // 함수를 실행할거에요 -> 보스 AI 두뇌인 ExecuteAILogic을
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════
|
||||
// 🧠 AI 핵심 로직
|
||||
// ════════════════════════════════════════
|
||||
|
||||
protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> 보스 AI 두뇌인 ExecuteAILogic을
|
||||
{
|
||||
if (isPerformingAction || !isBattleStarted || target == null) return; // 조건이 맞으면 중단할거에요 -> 연출중, 전투전, 타겟없음 중 하나라도 해당되면
|
||||
|
||||
if (isWeaponless) // 조건이 맞으면 실행할거에요 -> 쇠공을 던져서 맨손이라면
|
||||
{
|
||||
RetrieveWeaponLogic(); // 함수를 실행할거에요 -> 쇠공을 주우러 가는 로직을
|
||||
return; // 중단할거에요 -> 무기 줍기가 우선이므로 아래 로직을
|
||||
}
|
||||
|
||||
if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면 AI 판단을
|
||||
|
||||
if (_timer > 0) // 조건이 맞으면 실행할거에요 -> 공격 쿨타임이 남아있다면
|
||||
{
|
||||
_timer -= Time.deltaTime; // 값을 뺄거에요 -> 쿨타임 타이머에서 지난 프레임 시간만큼을
|
||||
}
|
||||
|
||||
float distToPlayer = Vector3.Distance(transform.position, target.position); // 거리를 계산할거에요 -> 보스와 플레이어 사이의 거리를
|
||||
|
||||
if (agent == null || !agent.enabled || !agent.isOnNavMesh) return; // 조건이 맞으면 중단할거에요 -> 길찾기가 불가능한 상태라면
|
||||
|
||||
if (distToPlayer > attackRange) // 조건이 맞으면 실행할거에요 -> 플레이어가 공격 사거리 밖이라면
|
||||
{
|
||||
agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 걸어가라고
|
||||
agent.SetDestination(target.position); // 명령을 내릴거에요 -> 길찾기 목표를 플레이어 위치로
|
||||
LookAtTarget(target.position); // 함수를 실행할거에요 -> 플레이어를 바라보는 LookAtTarget을
|
||||
UpdateMovementAnimation(); // 함수를 실행할거에요 -> 걷기 애니메이션을 맞추는 UpdateMovementAnimation을
|
||||
}
|
||||
else // 조건이 틀리면 실행할거에요 -> 플레이어가 사거리 안에 있다면
|
||||
{
|
||||
agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고
|
||||
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 이동 속도를 완전한 0으로
|
||||
LookAtTarget(target.position); // 함수를 실행할거에요 -> 플레이어를 바라보는 LookAtTarget을
|
||||
|
||||
if (animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 존재한다면
|
||||
{
|
||||
AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 변수를 선언할거에요 -> 현재 재생 중인 애니메이션 상태를
|
||||
if (state.IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 걷기 모션이 재생 중이라면
|
||||
{
|
||||
animator.Play(anim_Idle); // 애니메이션을 바꿀거에요 -> 대기 모션으로
|
||||
}
|
||||
}
|
||||
|
||||
if (_timer <= 0) // 조건이 맞으면 실행할거에요 -> 공격 쿨타임이 0 이하라면
|
||||
{
|
||||
_timer = patternInterval; // 값을 넣을거에요 -> 쿨타임을 다시 패턴 대기 시간으로
|
||||
Debug.Log($"⏰ [Boss] 사거리 내 공격! 거리={distToPlayer:F1}m"); // 로그를 출력할거에요 -> 공격 시점과 거리 정보를
|
||||
DecideAttack(); // 함수를 실행할거에요 -> 어떤 공격을 할지 고르는 DecideAttack을
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════
|
||||
// 🎬 전투 입장
|
||||
// ════════════════════════════════════════
|
||||
|
||||
public void StartBossBattle() // 함수를 선언할거에요 -> 외부에서 전투를 시작시킬 수 있는 StartBossBattle을
|
||||
{
|
||||
if (isBattleStarted) return; // 조건이 맞으면 중단할거에요 -> 이미 전투가 시작되었다면 중복 실행을
|
||||
StartCoroutine(BattleStartRoutine()); // 코루틴을 실행할거에요 -> Roar 연출 후 전투를 시작하는 BattleStartRoutine을
|
||||
}
|
||||
|
||||
private IEnumerator BattleStartRoutine() // 코루틴 함수를 선언할거에요 -> Roar 연출과 전투 시작을 담당하는 BattleStartRoutine을
|
||||
{
|
||||
Debug.Log("🔥 [Boss] Roar 시작!"); // 로그를 출력할거에요 -> Roar가 시작되었다는 메시지를
|
||||
isPerformingAction = true; // 상태를 바꿀거에요 -> 연출 중 플래그를 참(true)으로
|
||||
|
||||
if (bossHealthBar != null) bossHealthBar.SetActive(true); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에 켜기를
|
||||
if (counterSystem != null) counterSystem.InitializeBattle(); // 조건이 맞으면 실행할거에요 -> 카운터 시스템이 있다면 전투 초기화를
|
||||
|
||||
if (animator != null) animator.Play(anim_Roar, 0, 0f); // 조건이 맞으면 실행할거에요 -> Roar 애니메이션을 처음부터 재생을
|
||||
|
||||
yield return new WaitForSeconds(2.0f); // 기다릴거에요 -> Roar 애니메이션이 끝날 2초의 시간을
|
||||
|
||||
if (animator != null) animator.Play(anim_Idle, 0, 0f); // 조건이 맞으면 실행할거에요 -> Roar가 끝났으니 대기 모션으로 전환을
|
||||
|
||||
isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 중 플래그를 거짓(false)으로
|
||||
isBattleStarted = true; // 상태를 바꿀거에요 -> 전투 시작 플래그를 참(true)으로
|
||||
|
||||
Debug.Log("⚔️ [Boss] Roar 종료 → 전투 루프 시작!"); // 로그를 출력할거에요 -> 전투 루프가 시작되었다는 메시지를
|
||||
|
||||
if (agent != null) // 조건이 맞으면 실행할거에요 -> 길찾기 에이전트가 존재한다면
|
||||
{
|
||||
agent.enabled = true; // 기능을 켤거에요 -> 길찾기 에이전트를 활성화(true)로
|
||||
int retry = 0; // 변수를 선언할거에요 -> 재시도 횟수를 셀 retry를 0으로
|
||||
while (!agent.isOnNavMesh && retry < 10) // 반복할거에요 -> NavMesh에 안 닿았고 10번 미만인 동안
|
||||
{
|
||||
retry++; // 값을 증가시킬거에요 -> 재시도 횟수를 1만큼
|
||||
yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초의 시간을
|
||||
}
|
||||
|
||||
if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 올라갔다면
|
||||
{
|
||||
agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 추격을 시작하라고
|
||||
Debug.Log("✅ [Boss] NavMesh 연결 완료, 추격 시작"); // 로그를 출력할거에요 -> NavMesh 연결 성공 메시지를
|
||||
}
|
||||
else // 조건이 틀리면 실행할거에요 -> NavMesh에 못 올라갔다면
|
||||
{
|
||||
Debug.LogError("❌ [Boss] NavMesh 연결 실패! 보스 위치를 확인하세요!"); // 에러를 출력할거에요 -> 연결 실패 경고를
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════
|
||||
// 🚶 이동 애니메이션 & 타겟팅
|
||||
// ════════════════════════════════════════
|
||||
|
||||
private void UpdateMovementAnimation() // 함수를 선언할거에요 -> 이동 상태에 맞는 애니메이션을 재생하는 UpdateMovementAnimation을
|
||||
{
|
||||
if (animator == null || isPerformingAction || isAttacking || isHit || isDead) return; // 조건이 맞으면 중단할거에요 -> 애니메이션을 바꿀 수 없는 상태라면
|
||||
|
||||
AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 변수를 선언할거에요 -> 현재 재생 중인 애니메이션 상태를
|
||||
|
||||
if (agent != null && agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 현재 이동 속도가 0.1보다 크다면
|
||||
{
|
||||
if (!state.IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 아직 걷기 모션이 아니라면
|
||||
animator.Play(anim_Walk); // 애니메이션을 바꿀거에요 -> 걷기 모션으로
|
||||
}
|
||||
else // 조건이 틀리면 실행할거에요 -> 거의 멈춰있다면
|
||||
{
|
||||
if (!state.IsName(anim_Idle) && !state.IsName(anim_Roar)) // 조건이 맞으면 실행할거에요 -> 대기나 포효 모션이 아니라면
|
||||
animator.Play(anim_Idle); // 애니메이션을 바꿀거에요 -> 대기 모션으로
|
||||
}
|
||||
}
|
||||
|
||||
private void LookAtTarget(Vector3 targetPos) // 함수를 선언할거에요 -> 타겟을 부드럽게 쳐다보는 LookAtTarget을
|
||||
{
|
||||
Vector3 dir = targetPos - transform.position; // 벡터를 계산할거에요 -> 내 위치에서 타겟까지의 방향을
|
||||
dir.y = 0; // 값을 바꿀거에요 -> 위아래로 고개가 꺾이지 않게 y를 0으로
|
||||
if (dir != Vector3.zero) // 조건이 맞으면 실행할거에요 -> 방향이 0이 아니라면
|
||||
{
|
||||
transform.rotation = Quaternion.Slerp( // 회전시킬거에요 -> 현재 회전에서 타겟 방향으로 부드럽게
|
||||
transform.rotation, // 시작값으로 사용할거에요 -> 현재 회전값을
|
||||
Quaternion.LookRotation(dir), // 목표값으로 사용할거에요 -> 타겟을 향하는 회전값을
|
||||
Time.deltaTime * 5f // 속도를 지정할거에요 -> 프레임 시간의 5배 속도로
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════
|
||||
// 🧠 공격 판단
|
||||
// ════════════════════════════════════════
|
||||
|
||||
private void DecideAttack() // 함수를 선언할거에요 -> 어떤 공격 패턴을 쓸지 고르는 DecideAttack을
|
||||
{
|
||||
if (target != null) LookAtTarget(target.position); // 조건이 맞으면 실행할거에요 -> 타겟이 있다면 그쪽을 바라보기를
|
||||
|
||||
string patternName = (counterSystem != null) ? counterSystem.SelectBossPattern() : "Normal"; // 변수를 선언할거에요 -> 카운터 시스템에서 받은 패턴 이름을 patternName에
|
||||
Debug.Log($"🧠 [Boss] 패턴 선택: {patternName}"); // 로그를 출력할거에요 -> 선택된 패턴 이름을
|
||||
|
||||
switch (patternName) // 분기할거에요 -> 받아온 패턴 이름에 따라서
|
||||
{
|
||||
case "DashAttack": StartCoroutine(Pattern_DashAttack()); break; // 일치하면 실행할거에요 -> 돌진 공격을
|
||||
case "Smash": StartCoroutine(Pattern_SmashAttack()); break; // 일치하면 실행할거에요 -> 내려찍기 공격을
|
||||
case "ShieldWall": StartCoroutine(Pattern_ShieldWall()); break; // 일치하면 실행할거에요 -> 휩쓸기 공격을
|
||||
default: StartCoroutine(Pattern_ThrowBall()); break; // 맞는게 없으면 실행할거에요 -> 공 던지기 공격을
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════
|
||||
// 🏃 무기 회수
|
||||
// ════════════════════════════════════════
|
||||
|
||||
private void RetrieveWeaponLogic() // 함수를 선언할거에요 -> 던진 쇠공을 주우러 가는 RetrieveWeaponLogic을
|
||||
{
|
||||
if (ironBall == null || !agent.enabled || !agent.isOnNavMesh) return; // 조건이 맞으면 중단할거에요 -> 쇠공이 없거나 길찾기가 불가능하다면
|
||||
|
||||
agent.SetDestination(ironBall.transform.position); // 명령을 내릴거에요 -> 길찾기 목표를 쇠공의 위치로
|
||||
agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 걸어가라고
|
||||
UpdateMovementAnimation(); // 함수를 실행할거에요 -> 걸어갈 때 이동 애니메이션이 나오도록
|
||||
|
||||
if (Vector3.Distance(transform.position, ironBall.transform.position) <= 3.0f && !isAttacking) // 조건이 맞으면 실행할거에요 -> 쇠공과 3m 이내이고 공격 중이 아니라면
|
||||
StartCoroutine(PickUpBallRoutine()); // 코루틴을 실행할거에요 -> 바닥의 쇠공을 줍는 PickUpBallRoutine을
|
||||
}
|
||||
|
||||
private IEnumerator PickUpBallRoutine() // 코루틴 함수를 선언할거에요 -> 쇠공 줍기 행동인 PickUpBallRoutine을
|
||||
{
|
||||
OnAttackStart(); // 상태를 켤거에요 -> 다른 행동과 겹치지 않게 공격 플래그를
|
||||
if (agent != null && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 조건이 맞으면 실행할거에요 -> 줍기 위해 이동을 멈추고 속도를 0으로
|
||||
if (animator != null) animator.Play(anim_Pickup); // 조건이 맞으면 실행할거에요 -> 줍는 애니메이션을
|
||||
yield return new WaitForSeconds(0.8f); // 기다릴거에요 -> 손이 바닥에 닿을 0.8초를
|
||||
|
||||
ironBall.transform.SetParent(handHolder); // 부모를 바꿀거에요 -> 쇠공을 보스의 손 밑으로
|
||||
ironBall.transform.localPosition = Vector3.zero; // 위치를 바꿀거에요 -> 쇠공의 위치를 손의 정중앙으로
|
||||
ironBall.transform.localRotation = Quaternion.identity; // 회전을 바꿀거에요 -> 쇠공의 회전을 기본값으로
|
||||
if (ballRb != null) { ballRb.isKinematic = true; ballRb.velocity = Vector3.zero; } // 조건이 맞으면 실행할거에요 -> 쇠공의 물리를 끄고 속도를 0으로
|
||||
|
||||
yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 일어서는 애니메이션이 끝날 1초를
|
||||
isWeaponless = false; // 상태를 바꿀거에요 -> 무기를 다시 쥐었으므로 맨손 상태를 거짓으로
|
||||
OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 처리를
|
||||
if (agent != null && agent.isOnNavMesh) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 다시 추격을 위해 이동 정지 해제를
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════
|
||||
// ✅ 공격 시작/종료 처리 (핵심)
|
||||
// ════════════════════════════════════════
|
||||
|
||||
public override void OnAttackStart() // 함수를 덮어씌워 실행할거에요 -> 공격 시작 처리를 하는 OnAttackStart를
|
||||
{
|
||||
isAttacking = true; // 상태를 바꿀거에요 -> 공격 중임을 참으로
|
||||
isPerformingAction = true; // 상태를 바꿀거에요 -> 다른 행동 불가 상태로
|
||||
if (agent != null) agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고
|
||||
}
|
||||
|
||||
public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 처리를 하는 OnAttackEnd를
|
||||
{
|
||||
StartCoroutine(RecoverRoutine()); // 코루틴을 시작할거에요 -> 후딜레이(회복) 처리를 위한 RecoverRoutine을
|
||||
}
|
||||
|
||||
private IEnumerator RecoverRoutine() // 코루틴 함수를 선언할거에요 -> 공격 후 대기 시간을 갖는 RecoverRoutine을
|
||||
{
|
||||
isAttacking = false; // 상태를 바꿀거에요 -> 공격이 끝났으므로 공격 중 상태를 거짓으로
|
||||
isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참으로 (이때는 추격 안 함)
|
||||
|
||||
if (animator != null) animator.Play(anim_Idle); // 조건이 맞으면 실행할거에요 -> 대기 모션으로 전환을
|
||||
|
||||
// 딜레이 시간 (여기서 patternInterval 만큼 멍때림)
|
||||
yield return new WaitForSeconds(patternInterval); // 기다릴거에요 -> 설정된 패턴 간격 시간만큼
|
||||
|
||||
isResting = false; // 상태를 바꿀거에요 -> 휴식이 끝났으므로 거짓으로
|
||||
isPerformingAction = false; // 상태를 바꿀거에요 -> 이제 다시 다른 행동이 가능하도록 거짓으로
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════
|
||||
// ⚔️ 공격 패턴들
|
||||
// ════════════════════════════════════════
|
||||
|
||||
// 패턴 1: 돌진 공격
|
||||
private IEnumerator Pattern_DashAttack() // 코루틴 함수를 선언할거에요 -> 돌진 패턴인 Pattern_DashAttack을
|
||||
{
|
||||
OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를
|
||||
if (agent != null) agent.enabled = false; // 조건이 맞으면 실행할거에요 -> 돌진 시 물리력을 위해 길찾기를 끄기로
|
||||
|
||||
if (animator != null) animator.Play(anim_DashReady, 0, 0f); // 조건이 맞으면 실행할거에요 -> 돌진 준비 모션을
|
||||
yield return new WaitForSeconds(0.5f); // 기다릴거에요 -> 준비 포즈를 취할 0.5초를
|
||||
|
||||
if (rb != null) { rb.isKinematic = false; rb.velocity = transform.forward * 20f; } // 조건이 맞으면 실행할거에요 -> 물리를 켜고 앞으로 속도 20으로 돌진을
|
||||
if (animator != null) animator.Play(anim_DashGo, 0, 0f); // 조건이 맞으면 실행할거에요 -> 돌진 애니메이션을
|
||||
yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 돌진이 끝날 1초를
|
||||
|
||||
if (rb != null) { rb.velocity = Vector3.zero; rb.isKinematic = true; } // 조건이 맞으면 실행할거에요 -> 속도를 0으로 멈추고 물리를 끄기로
|
||||
if (agent != null && isBattleStarted) agent.enabled = true; // 조건이 맞으면 실행할거에요 -> 전투 중이면 길찾기를 다시 켜기로
|
||||
OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를
|
||||
}
|
||||
|
||||
// 패턴 2: 내려찍기
|
||||
private IEnumerator Pattern_SmashAttack() // 코루틴 함수를 선언할거에요 -> 내려찍기 패턴인 Pattern_SmashAttack을
|
||||
{
|
||||
OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를
|
||||
if (animator != null) animator.Play(anim_SmashCharge, 0, 0f); // 조건이 맞으면 실행할거에요 -> 기를 모으는 모션을
|
||||
yield return new WaitForSeconds(1.2f); // 기다릴거에요 -> 기를 모을 1.2초를
|
||||
if (animator != null) animator.Play(anim_SmashImpact, 0, 0f); // 조건이 맞으면 실행할거에요 -> 내려찍는 모션을
|
||||
yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 자세를 가다듬을 1초를
|
||||
OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를
|
||||
}
|
||||
|
||||
// 패턴 3: 휩쓸기
|
||||
private IEnumerator Pattern_ShieldWall() // 코루틴 함수를 선언할거에요 -> 휩쓸기 패턴인 Pattern_ShieldWall을
|
||||
{
|
||||
OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를
|
||||
if (animator != null) animator.Play(anim_Sweep, 0, 0f); // 조건이 맞으면 실행할거에요 -> 휩쓰는 모션을
|
||||
yield return new WaitForSeconds(2.0f); // 기다릴거에요 -> 휘두르기가 끝날 2초를
|
||||
OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를
|
||||
}
|
||||
|
||||
// 패턴 4: 공 던지기
|
||||
private IEnumerator Pattern_ThrowBall() // 코루틴 함수를 선언할거에요 -> 쇠공 던지기 패턴인 Pattern_ThrowBall을
|
||||
{
|
||||
OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를
|
||||
if (animator != null) animator.Play(anim_Throw, 0, 0f); // 조건이 맞으면 실행할거에요 -> 던지는 모션을
|
||||
yield return new WaitForSeconds(0.5f); // 기다릴거에요 -> 공을 던지기 직전인 0.5초를
|
||||
|
||||
if (ironBall != null && ballRb != null) // 조건이 맞으면 실행할거에요 -> 쇠공과 물리 엔진이 존재한다면
|
||||
{
|
||||
ironBall.transform.SetParent(null); // 부모를 바꿀거에요 -> 쇠공을 손에서 완전히 분리로
|
||||
ballRb.isKinematic = false; // 기능을 켤거에요 -> 쇠공의 물리 연산을 활성화로
|
||||
Vector3 dir = (target.position - transform.position).normalized; // 벡터를 계산할거에요 -> 플레이어를 향하는 방향을
|
||||
ballRb.AddForce(dir * 20f + Vector3.up * 5f, ForceMode.Impulse); // 힘을 가할거에요 -> 앞으로 20, 위로 5의 포물선 힘을
|
||||
ballRb.angularDrag = 5f; // 마찰을 줄거에요 -> 쇠공이 멀리 안 굴러가게 회전 마찰 5를
|
||||
}
|
||||
|
||||
isWeaponless = true; // 상태를 바꿀거에요 -> 던졌으므로 맨손 상태를 참으로
|
||||
yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 던지고 자세를 가다듬을 1초를
|
||||
OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════
|
||||
// 💥 피격 / 사망
|
||||
// ════════════════════════════════════════
|
||||
|
||||
protected override void OnStartHit() // 함수를 덮어씌워 실행할거에요 -> 보스가 맞았을 때 호출되는 OnStartHit을
|
||||
{
|
||||
// 보스는 피격되어도 행동이 끊기지 않습니다 (슈퍼아머)
|
||||
// 만약 끊기게 하려면 여기서 isPerformingAction = false;
|
||||
}
|
||||
|
||||
protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 보스 체력이 0이 되면 호출되는 OnDie를
|
||||
{
|
||||
isBattleStarted = false; // 상태를 바꿀거에요 -> 전투가 끝났으므로 전투 플래그를 거짓으로
|
||||
isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 플래그를 거짓으로
|
||||
if (bossHealthBar != null) bossHealthBar.SetActive(false); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에서 끄기를
|
||||
GiveBossXP(); // 함수를 실행할거에요 -> 경험치를 주는 GiveBossXP를
|
||||
base.OnDie(); // 부모 클래스의 사망 처리를 호출할거에요 -> 애니메이션, 시체 제거 등
|
||||
}
|
||||
|
||||
private void GiveBossXP() // 함수를 선언할거에요 -> 보스 처치 보상을 주는 GiveBossXP를
|
||||
{
|
||||
Debug.Log("[BossController] 보스 처치 시도 중..."); // 로그를 출력할거에요 -> 보스 처치 시도 메시지를
|
||||
if (ObsessionSystem.instance != null) // 조건이 맞으면 실행할거에요 -> 경험치 시스템이 존재한다면
|
||||
ObsessionSystem.instance.AddRunXP(100); // 함수를 실행할거에요 -> 경험치 100을 주는 AddRunXP를
|
||||
else // 조건이 틀리면 실행할거에요 -> 시스템을 못 찾았다면
|
||||
Debug.LogError("[BossController] ObsessionSystem 인스턴스를 찾을 수 없습니다!"); // 에러를 출력할거에요 -> 시스템 누락 경고를
|
||||
Debug.Log("<color=red>[BossController] 보스 처치 처리 완료!</color>"); // 로그를 출력할거에요 -> 빨간 글씨로 처치 완료 메시지를
|
||||
}
|
||||
}
|
||||
[Header("--- 🎬 스킬 애니메이션 이름 ---")] // 인스펙터 제목을 달거에요 -> 스킬 애니메이션 설정을
|
||||
[SerializeField] private string anim_DashReady = "Skill_Dash_Ready"; // 변수를 선언할거에요 -> 돌진 준비 애니 이름을
|
||||
[SerializeField] private string anim_DashGo = "Skill_Dash_Go"; // 변수를 선언할거에요 -> 돌진 애니 이름을
|
||||
[SerializeField] private string anim_SmashCharge = "Skill_Smash_Charge"; // 변수를 선언할거에요 -> 찍기 차징 애니 이름을
|
||||
[SerializeField] private string anim_SmashImpact = "Skill_Smash_Impact"; // 변수를 선언할거에요 -> 찍기 타격 애니 이름을
|
||||
[SerializeField] private string anim_Sweep = "Skill_Sweep"; // 변수를 선언할거에요 -> 휩쓸기 애니 이름을
|
||||
|
||||
[Header("--- 🔍 디버그 (읽기 전용) ---")] // 인스펙터 제목을 달거에요 -> 디버그 정보를
|
||||
[SerializeField] private string debugState = "대기 중"; // 변수를 선언할거에요 -> 현재 상태를 보여줄 텍스트를
|
||||
|
||||
private float _timer; // 변수를 선언할거에요 -> 패턴 쿨타임용 타이머를
|
||||
private Rigidbody rb; // 변수를 선언할거에요 -> 보스의 물리 몸체를
|
||||
private Rigidbody ballRb; // 변수를 선언할거에요 -> 쇠공의 물리 몸체를
|
||||
|
||||
private bool isBattleStarted = false; // 변수를 설정할거에요 -> 전투 시작 여부를 (기본 거짓)
|
||||
private bool isWeaponless = false; // 변수를 설정할거에요 -> 무기 분실 여부를 (기본 거짓)
|
||||
private bool isPerformingAction = false; // 변수를 설정할거에요 -> 행동 수행 중 여부를 (기본 거짓)
|
||||
private Transform target; // 변수를 선언할거에요 -> 공격 대상(플레이어)을
|
||||
|
||||
private readonly Dictionary<string, float> _clipLengthCache = new Dictionary<string, float>(); // 딕셔너리를 선언할거에요 -> 애니메이션 길이를 저장할 캐시를
|
||||
|
||||
protected override void Awake() // 함수를 실행할거에요 -> 객체 생성 시 초기화를
|
||||
{ // 코드 블록을 시작할거에요 -> Awake 범위를
|
||||
base.Awake(); // 부모의 Awake를 실행할거에요
|
||||
rb = GetComponent<Rigidbody>(); // 가져올거에요 -> 내 몸의 Rigidbody를
|
||||
if (ironBall != null) ballRb = ironBall.GetComponent<Rigidbody>(); // 조건이 맞으면 가져올거에요 -> 쇠공의 Rigidbody를
|
||||
} // 코드 블록을 끝낼거에요 -> Awake를
|
||||
|
||||
private void Start() // 함수를 실행할거에요 -> 게임 시작 시 초기화를
|
||||
{ // 코드 블록을 시작할거에요 -> Start 범위를
|
||||
CacheAllClipLengths(); // 함수를 실행할거에요 -> 애니메이션 길이를 미리 저장하는 기능을
|
||||
StartBossBattle(); // 함수를 실행할거에요 -> 보스전을 시작하는 기능을
|
||||
} // 코드 블록을 끝낼거에요 -> Start를
|
||||
|
||||
private void CacheAllClipLengths() // 함수를 정의할거에요 -> 애니메이션 길이를 캐싱하는 로직을
|
||||
{ // 코드 블록을 시작할거에요 -> CacheAllClipLengths 범위를
|
||||
_clipLengthCache.Clear(); // 비울거에요 -> 기존 캐시 데이터를
|
||||
|
||||
if (animator == null || animator.runtimeAnimatorController == null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 없다면
|
||||
{ // 코드 블록을 시작할거에요 -> 에러 처리 범위를
|
||||
Debug.LogError("❌ [Boss] Animator 또는 RuntimeAnimatorController가 없습니다! 클립 길이를 캐싱할 수 없습니다."); // 에러를 출력할거에요
|
||||
return; // 중단할거에요
|
||||
} // 코드 블록을 끝낼거에요 -> 에러 처리 끝
|
||||
|
||||
AnimationClip[] clips = animator.runtimeAnimatorController.animationClips; // 배열을 가져올거에요 -> 모든 애니메이션 클립을
|
||||
|
||||
foreach (AnimationClip clip in clips) // 반복할거에요 -> 모든 클립을 돌면서
|
||||
{ // 코드 블록을 시작할거에요 -> foreach 범위를
|
||||
|
||||
if (!_clipLengthCache.ContainsKey(clip.name)) // 조건이 맞으면 실행할거에요 -> 이름이 캐시에 없다면
|
||||
{ // 코드 블록을 시작할거에요 -> 캐시 저장 범위를
|
||||
_clipLengthCache[clip.name] = clip.length; // 저장할거에요 -> 원본 이름을 키로 길이를
|
||||
} // 코드 블록을 끝낼거에요 -> 캐시 저장 끝
|
||||
|
||||
int pipeIndex = clip.name.IndexOf('|'); // 위치를 찾을거에요 -> "|" 기호의 위치를
|
||||
|
||||
if (pipeIndex >= 0 && pipeIndex < clip.name.Length - 1) // 조건이 맞으면 실행할거에요 -> "|" 기호가 존재하면
|
||||
{ // 코드 블록을 시작할거에요 -> 단축 이름 처리 범위를
|
||||
string shortName = clip.name.Substring(pipeIndex + 1); // 추출할거에요 -> "|" 뒤의 짧은 이름을
|
||||
|
||||
if (!_clipLengthCache.ContainsKey(shortName)) // 조건이 맞으면 실행할거에요 -> 짧은 이름이 캐시에 없다면
|
||||
{ // 코드 블록을 시작할거에요 -> 단축 이름 캐시 저장 범위를
|
||||
_clipLengthCache[shortName] = clip.length; // 저장할거에요 -> 짧은 이름도 키로 등록
|
||||
} // 코드 블록을 끝낼거에요 -> 단축 이름 저장 끝
|
||||
} // 코드 블록을 끝낼거에요 -> 단축 이름 처리 끝
|
||||
|
||||
Debug.Log($"🎬 [Boss] 클립 캐싱: \"{clip.name}\" = {clip.length:F2}초"); // 로그를 찍을거에요 -> 캐싱 정보를
|
||||
} // 코드 블록을 끝낼거에요 -> foreach 끝
|
||||
|
||||
Debug.Log($"✅ [Boss] 총 {_clipLengthCache.Count}개 키 캐싱 완료! (원본 + 단축이름 포함)"); // 로그를 찍을거에요 -> 완료 정보를
|
||||
} // 코드 블록을 끝낼거에요 -> CacheAllClipLengths를
|
||||
|
||||
private float GetClipLength(string clipName, float fallback = 1.0f) // 함수를 정의할거에요 -> 이름으로 길이를 찾는 기능을
|
||||
{ // 코드 블록을 시작할거에요 -> GetClipLength 범위를
|
||||
if (_clipLengthCache.TryGetValue(clipName, out float length)) // 조건이 맞으면 실행할거에요 -> 캐시에서 바로 찾았다면
|
||||
{ // 코드 블록을 시작할거에요 -> 즉시 반환 범위를
|
||||
return length; // 반환할거에요 -> 찾은 길이를
|
||||
} // 코드 블록을 끝낼거에요 -> 즉시 반환 끝
|
||||
|
||||
foreach (KeyValuePair<string, float> kvp in _clipLengthCache) // 반복할거에요 -> 캐시 전체를 뒤져서
|
||||
{ // 코드 블록을 시작할거에요 -> 부분 매칭 범위를
|
||||
if (kvp.Key.EndsWith(clipName)) // 조건이 맞으면 실행할거에요 -> 이름 끝부분이 일치한다면
|
||||
{ // 코드 블록을 시작할거에요 -> 매칭 성공 범위를
|
||||
_clipLengthCache[clipName] = kvp.Value; // 저장할거에요 -> 다음 검색을 위해 짧은 이름도 캐싱
|
||||
Debug.Log($"🔗 [Boss] 부분 매칭 성공: \"{clipName}\" → \"{kvp.Key}\" ({kvp.Value:F2}초)"); // 로그를 찍을거에요
|
||||
return kvp.Value; // 반환할거에요 -> 매칭된 길이를
|
||||
} // 코드 블록을 끝낼거에요 -> 매칭 성공 끝
|
||||
} // 코드 블록을 끝낼거에요 -> 부분 매칭 끝
|
||||
|
||||
Debug.LogWarning($"⚠️ [Boss] 클립 \"{clipName}\"을 찾지 못했습니다! fallback={fallback:F2}초 사용"); // 경고를 찍을거에요 -> 못 찾았을 때
|
||||
return fallback; // 반환할거에요 -> 기본값을
|
||||
} // 코드 블록을 끝낼거에요 -> GetClipLength를
|
||||
|
||||
private IEnumerator PlayAndWait(string animName, float fallback = 1.0f) // 코루틴을 정의할거에요 -> 애니 재생 후 길이만큼 대기하는 기능을
|
||||
{ // 코드 블록을 시작할거에요 -> PlayAndWait 범위를
|
||||
if (animator == null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 없다면
|
||||
{ // 코드 블록을 시작할거에요 -> 예외 처리 범위를
|
||||
Debug.LogWarning($"⚠️ [Boss] Animator 없음! \"{animName}\" fallback={fallback}초 대기"); // 경고를 찍을거에요
|
||||
yield return new WaitForSeconds(Mathf.Max(0.1f, fallback)); // 대기할거에요 -> 최소 0.1초 또는 기본값만큼
|
||||
yield break; // 종료할거에요
|
||||
} // 코드 블록을 끝낼거에요 -> 예외 처리 끝
|
||||
|
||||
int stateHash = Animator.StringToHash(animName); // 계산할거에요 -> 상태 이름의 해시값을
|
||||
bool stateExists = animator.HasState(0, stateHash); // 확인할거에요 -> 상태가 실제로 존재하는지
|
||||
|
||||
if (!stateExists) // 조건이 맞으면 실행할거에요 -> 상태가 존재하지 않는다면
|
||||
{ // 코드 블록을 시작할거에요 -> 에러 처리 범위를
|
||||
Debug.LogError($"❌ [Boss] Animator에 State \"{animName}\"이 없습니다! Animator Controller를 확인하세요."); // 에러를 찍을거에요
|
||||
yield return new WaitForSeconds(Mathf.Max(0.1f, fallback)); // 대기할거에요 -> 기본값만큼
|
||||
yield break; // 종료할거에요
|
||||
} // 코드 블록을 끝낼거에요 -> 에러 처리 끝
|
||||
|
||||
animator.Play(animName, 0, 0f); // 재생할거에요 -> 해당 애니메이션을 처음부터
|
||||
yield return null; // 대기할거에요 -> 1프레임 동안 (상태 갱신)
|
||||
|
||||
float duration = GetClipLength(animName, -1f); // 가져올거에요 -> 캐싱된 길이를
|
||||
|
||||
if (duration < 0f) // 조건이 맞으면 실행할거에요 -> 캐시에 없는 경우
|
||||
{ // 코드 블록을 시작할거에요 -> State 길이로 보정 범위를
|
||||
AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0); // 가져올거에요 -> 현재 상태 정보를
|
||||
duration = stateInfo.length; // 가져올거에요 -> 상태의 길이를
|
||||
|
||||
if (duration > 0f) // 조건이 맞으면 실행할거에요 -> 유효한 길이면
|
||||
{ // 코드 블록을 시작할거에요 -> 캐싱 범위를
|
||||
_clipLengthCache[animName] = duration; // 저장할거에요 -> 캐시에 등록
|
||||
Debug.Log($"🎬 [Boss] State에서 길이 확인 & 캐싱: \"{animName}\" = {duration:F2}초"); // 로그를 찍을거에요
|
||||
} // 코드 블록을 끝낼거에요 -> 캐싱 끝
|
||||
else // 조건이 틀리면 실행할거에요 -> 길이가 이상하면
|
||||
{ // 코드 블록을 시작할거에요 -> fallback 적용 범위를
|
||||
duration = Mathf.Max(0.1f, fallback); // 설정할거에요 -> 기본값으로 보장
|
||||
Debug.LogWarning($"⚠️ [Boss] State \"{animName}\" 길이 비정상({stateInfo.length}초)! fallback={duration}초 사용"); // 경고를 찍을거에요
|
||||
} // 코드 블록을 끝낼거에요 -> fallback 적용 끝
|
||||
} // 코드 블록을 끝낼거에요 -> State 길이 보정 끝
|
||||
|
||||
float remaining = duration - Time.deltaTime; // 계산할거에요 -> 1프레임을 뺀 남은 시간을
|
||||
|
||||
if (remaining > 0f) // 조건이 맞으면 실행할거에요 -> 남은 시간이 양수면
|
||||
{ // 코드 블록을 시작할거에요 -> 대기 범위를
|
||||
yield return new WaitForSeconds(remaining); // 기다릴거에요 -> 남은 시간만큼
|
||||
} // 코드 블록을 끝낼거에요 -> 대기 끝
|
||||
} // 코드 블록을 끝낼거에요 -> PlayAndWait를
|
||||
|
||||
protected override void Init() // 함수를 정의할거에요 -> 초기화 설정을
|
||||
{ // 코드 블록을 시작할거에요 -> Init 범위를
|
||||
base.Init(); // 부모의 초기화를 호출할거에요
|
||||
_timer = 0f; // 초기화할거에요 -> 타이머를 0으로
|
||||
isBattleStarted = false; // 설정할거에요 -> 전투 시작 전 상태로
|
||||
isWeaponless = false; // 설정할거에요 -> 무기 소지 상태로
|
||||
isPerformingAction = false; // 설정할거에요 -> 행동 전 상태로
|
||||
IsAggroed = true; // 설정할거에요 -> 어그로 켠 상태로
|
||||
mobRenderer = null; // 초기화할거에요 -> 렌더러 참조를
|
||||
optimizationDistance = 9999f; // 설정할거에요 -> 최적화 거리를 무한대로
|
||||
|
||||
GameObject playerObj = GameObject.FindWithTag("Player"); // 찾을거에요 -> 플레이어 태그 오브젝트를
|
||||
|
||||
if (playerObj != null) // 조건이 맞으면 실행할거에요 -> 플레이어를 찾았다면
|
||||
{ // 코드 블록을 시작할거에요 -> 태그 탐색 성공 범위를
|
||||
target = playerObj.transform; // 설정할거에요 -> 타겟 위치를
|
||||
playerTransform = playerObj.transform; // 설정할거에요 -> 부모의 타겟 위치도
|
||||
} // 코드 블록을 끝낼거에요 -> 태그 탐색 성공 끝
|
||||
else // 조건이 틀리면 실행할거에요 -> 태그로 못 찾았다면
|
||||
{ // 코드 블록을 시작할거에요 -> 타입 탐색 범위를
|
||||
PlayerMovement playerScript = FindObjectOfType<PlayerMovement>(); // 찾을거에요 -> 스크립트 타입으로 플레이어를
|
||||
|
||||
if (playerScript != null) // 조건이 맞으면 실행할거에요 -> 찾았다면
|
||||
{ // 코드 블록을 시작할거에요 -> 타입 탐색 성공 범위를
|
||||
target = playerScript.transform; // 설정할거에요 -> 타겟 위치를
|
||||
playerTransform = playerScript.transform; // 설정할거에요 -> 부모의 타겟 위치도
|
||||
} // 코드 블록을 끝낼거에요 -> 타입 탐색 성공 끝
|
||||
} // 코드 블록을 끝낼거에요 -> 타입 탐색 끝
|
||||
|
||||
if (target == null) // 조건이 맞으면 실행할거에요 -> 여전히 못 찾았다면
|
||||
{ // 코드 블록을 시작할거에요 -> 에러 처리 범위를
|
||||
Debug.LogError("❌ [Boss] 플레이어를 찾지 못했습니다! Player 태그 또는 PlayerMovement 확인!"); // 에러를 찍을거에요
|
||||
} // 코드 블록을 끝낼거에요 -> 에러 처리 끝
|
||||
|
||||
currentHP = maxHP; // 설정할거에요 -> 체력을 최대로
|
||||
StartCoroutine(SafeInitNavMesh()); // 시작할거에요 -> 내비메시 안전 초기화를
|
||||
|
||||
if (bossHealthBar != null) bossHealthBar.SetActive(false); // 끌거에요 -> 체력바 UI를
|
||||
if (ballRb != null) ballRb.isKinematic = true; // 설정할거에요 -> 쇠공 물리를 고정으로
|
||||
if (animator != null) animator.Play(anim_Idle); // 재생할거에요 -> 대기 애니메이션을
|
||||
} // 코드 블록을 끝낼거에요 -> Init을
|
||||
|
||||
private IEnumerator SafeInitNavMesh() // 코루틴을 정의할거에요 -> 내비메시 안전 초기화를
|
||||
{ // 코드 블록을 시작할거에요 -> SafeInitNavMesh 범위를
|
||||
yield return null; // 1프레임 쉴거에요
|
||||
|
||||
if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
|
||||
{ // 코드 블록을 시작할거에요 -> 에이전트 초기화 범위를
|
||||
int retry = 0; // 초기화할거에요 -> 재시도 횟수를
|
||||
|
||||
while (!agent.isOnNavMesh && retry < 5) // 반복할거에요 -> 메시 위에 안 올라왔고 시도가 5번 미만이면
|
||||
{ // 코드 블록을 시작할거에요 -> 재시도 루프 범위를
|
||||
retry++; // 늘릴거에요 -> 횟수를
|
||||
yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초 동안
|
||||
} // 코드 블록을 끝낼거에요 -> 재시도 루프 끝
|
||||
|
||||
if (agent.isOnNavMesh) agent.isStopped = true; // 조건이 맞으면 실행할거에요 -> 메시 위면 정지시킬거에요
|
||||
agent.enabled = false; // 끌거에요 -> 에이전트를 우선
|
||||
} // 코드 블록을 끝낼거에요 -> 에이전트 초기화 끝
|
||||
} // 코드 블록을 끝낼거에요 -> SafeInitNavMesh를
|
||||
|
||||
protected override void Update() // 함수를 실행할거에요 -> 매 프레임 업데이트를
|
||||
{ // 코드 블록을 시작할거에요 -> Update 범위를
|
||||
base.Update(); // 부모의 업데이트를 실행할거에요
|
||||
|
||||
if (!isDead) // 조건이 맞으면 실행할거에요 -> 살아있다면
|
||||
{ // 코드 블록을 시작할거에요 -> AI 실행 범위를
|
||||
ExecuteAILogic(); // 실행할거에요 -> AI 로직을
|
||||
} // 코드 블록을 끝낼거에요 -> AI 실행 끝
|
||||
} // 코드 블록을 끝낼거에요 -> Update를
|
||||
|
||||
protected override void ExecuteAILogic() // 함수를 정의할거에요 -> AI 행동 로직을
|
||||
{ // 코드 블록을 시작할거에요 -> ExecuteAILogic 범위를
|
||||
if (isPerformingAction || !isBattleStarted || target == null) return; // 중단할거에요 -> 행동 중이거나 시작 전이면
|
||||
|
||||
if (isWeaponless) // 조건이 맞으면 실행할거에요 -> 무기가 없다면
|
||||
{ // 코드 블록을 시작할거에요 -> 무기 회수 범위를
|
||||
RetrieveWeaponLogic(); // 실행할거에요 -> 무기 회수 로직을
|
||||
return; // 중단할거에요
|
||||
} // 코드 블록을 끝낼거에요 -> 무기 회수 끝
|
||||
|
||||
if (isAttacking || isHit || isResting) return; // 중단할거에요 -> 공격/피격/휴식 중이면
|
||||
|
||||
if (_timer > 0f) // 조건이 맞으면 실행할거에요 -> 타이머가 남았다면
|
||||
{ // 코드 블록을 시작할거에요 -> 타이머 감소 범위를
|
||||
_timer -= Time.deltaTime; // 줄일거에요 -> 시간을
|
||||
} // 코드 블록을 끝낼거에요 -> 타이머 감소 끝
|
||||
|
||||
float distToPlayer = Vector3.Distance(transform.position, target.position); // 계산할거에요 -> 플레이어와의 거리를
|
||||
|
||||
if (agent == null || !agent.enabled || !agent.isOnNavMesh) return; // 중단할거에요 -> 에이전트 상태가 정상이 아니면
|
||||
|
||||
if (distToPlayer > attackRange) // 조건이 맞으면 실행할거에요 -> 사거리보다 멀다면
|
||||
{ // 코드 블록을 시작할거에요 -> 추격 범위를
|
||||
agent.isStopped = false; // 켤거에요 -> 이동을
|
||||
agent.SetDestination(target.position); // 이동할거에요 -> 플레이어 위치로
|
||||
LookAtTarget(target.position); // 바라볼거에요 -> 플레이어를
|
||||
UpdateMovementAnimation(); // 실행할거에요 -> 이동 애니메이션 갱신을
|
||||
} // 코드 블록을 끝낼거에요 -> 추격 끝
|
||||
else // 조건이 틀리면 실행할거에요 -> 사거리 안이라면
|
||||
{ // 코드 블록을 시작할거에요 -> 사거리 내 행동 범위를
|
||||
agent.isStopped = true; // 멈출거에요 -> 이동을
|
||||
agent.velocity = Vector3.zero; // 멈출거에요 -> 관성을 없앨거에요
|
||||
|
||||
LookAtTarget(target.position); // 바라볼거에요 -> 플레이어를
|
||||
|
||||
if (animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면
|
||||
{ // 코드 블록을 시작할거에요 -> 애니 상태 체크 범위를
|
||||
AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 가져올거에요 -> 현재 상태를
|
||||
|
||||
if (state.IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 걷는 중이라면
|
||||
{ // 코드 블록을 시작할거에요 -> 대기로 전환 범위를
|
||||
animator.Play(anim_Idle); // 재생할거에요 -> 대기 애니를
|
||||
} // 코드 블록을 끝낼거에요 -> 대기로 전환 끝
|
||||
} // 코드 블록을 끝낼거에요 -> 애니 상태 체크 끝
|
||||
|
||||
if (_timer <= 0f) // 조건이 맞으면 실행할거에요 -> 공격 쿨타임이 끝났다면
|
||||
{ // 코드 블록을 시작할거에요 -> 공격 시작 범위를
|
||||
_timer = patternInterval; // 설정할거에요 -> 타이머를 다시
|
||||
Debug.Log($"⏰ [Boss] 사거리 내 공격! 거리={distToPlayer:F1}m"); // 로그를 찍을거에요
|
||||
DecideAttack(); // 실행할거에요 -> 공격 결정 기능을
|
||||
} // 코드 블록을 끝낼거에요 -> 공격 시작 끝
|
||||
} // 코드 블록을 끝낼거에요 -> 사거리 내 행동 끝
|
||||
} // 코드 블록을 끝낼거에요 -> ExecuteAILogic를
|
||||
|
||||
public void StartBossBattle() // 함수를 정의할거에요 -> 전투 시작 기능을
|
||||
{ // 코드 블록을 시작할거에요 -> StartBossBattle 범위를
|
||||
if (isBattleStarted) return; // 중단할거에요 -> 이미 시작했다면
|
||||
StartCoroutine(BattleStartRoutine()); // 시작할거에요 -> 시작 연출 코루틴을
|
||||
} // 코드 블록을 끝낼거에요 -> StartBossBattle를
|
||||
|
||||
private IEnumerator BattleStartRoutine() // 코루틴을 정의할거에요 -> 전투 시작 연출을
|
||||
{ // 코드 블록을 시작할거에요 -> BattleStartRoutine 범위를
|
||||
Debug.Log("🔥 [Boss] Roar 시작!"); // 로그를 찍을거에요
|
||||
|
||||
isPerformingAction = true; // 설정할거에요 -> 행동 수행 중으로
|
||||
if (bossHealthBar != null) bossHealthBar.SetActive(true); // 켤거에요 -> 체력바를
|
||||
if (counterSystem != null) counterSystem.InitializeBattle(); // 실행할거에요 -> 카운터 시스템 초기화를
|
||||
|
||||
yield return StartCoroutine(PlayAndWait(anim_Roar, 2.0f)); // 대기할거에요 -> 포효 애니가 끝날 때까지
|
||||
|
||||
if (animator != null) animator.Play(anim_Idle, 0, 0f); // 재생할거에요 -> 대기 애니를
|
||||
|
||||
isPerformingAction = false; // 해제할거에요 -> 행동 상태를
|
||||
isBattleStarted = true; // 설정할거에요 -> 전투 시작 상태로
|
||||
|
||||
Debug.Log("⚔️ [Boss] Roar 종료 → 전투 루프 시작!"); // 로그를 찍을거에요
|
||||
|
||||
if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
|
||||
{ // 코드 블록을 시작할거에요 -> NavMesh 연결 범위를
|
||||
agent.enabled = true; // 켤거에요 -> 에이전트를
|
||||
|
||||
int retry = 0; // 초기화할거에요 -> 재시도 횟수를
|
||||
|
||||
while (!agent.isOnNavMesh && retry < 10) // 반복할거에요 -> 메시 위에 없고 시도가 10번 미만이면
|
||||
{ // 코드 블록을 시작할거에요 -> 재시도 루프 범위를
|
||||
retry++; // 늘릴거에요 -> 횟수를
|
||||
yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초 동안
|
||||
} // 코드 블록을 끝낼거에요 -> 재시도 루프 끝
|
||||
|
||||
if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 메시 위라면
|
||||
{ // 코드 블록을 시작할거에요 -> 성공 처리 범위를
|
||||
agent.isStopped = false; // 켤거에요 -> 이동을
|
||||
Debug.Log("✅ [Boss] NavMesh 연결 완료, 추격 시작"); // 로그를 찍을거에요
|
||||
} // 코드 블록을 끝낼거에요 -> 성공 처리 끝
|
||||
else // 조건이 틀리면 실행할거에요 -> 연결 실패하면
|
||||
{ // 코드 블록을 시작할거에요 -> 실패 처리 범위를
|
||||
Debug.LogError("❌ [Boss] NavMesh 연결 실패! 보스 위치를 확인하세요!"); // 에러를 찍을거에요
|
||||
} // 코드 블록을 끝낼거에요 -> 실패 처리 끝
|
||||
} // 코드 블록을 끝낼거에요 -> NavMesh 연결 끝
|
||||
} // 코드 블록을 끝낼거에요 -> BattleStartRoutine를
|
||||
|
||||
private void UpdateMovementAnimation() // 함수를 정의할거에요 -> 이동 애니메이션 갱신을
|
||||
{ // 코드 블록을 시작할거에요 -> UpdateMovementAnimation 범위를
|
||||
if (animator == null || isPerformingAction || isAttacking || isHit || isDead) return; // 중단할거에요 -> 행동 불가 상태면
|
||||
|
||||
AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 가져올거에요 -> 상태 정보를
|
||||
|
||||
if (agent != null && agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 움직이고 있다면
|
||||
{ // 코드 블록을 시작할거에요 -> 걷기 애니 처리 범위를
|
||||
if (!state.IsName(anim_Walk)) animator.Play(anim_Walk); // 조건이 맞으면 재생할거에요 -> 걷기 애니를
|
||||
} // 코드 블록을 끝낼거에요 -> 걷기 애니 처리 끝
|
||||
else // 조건이 틀리면 실행할거에요 -> 멈춰 있다면
|
||||
{ // 코드 블록을 시작할거에요 -> 대기 애니 처리 범위를
|
||||
if (!state.IsName(anim_Idle) && !state.IsName(anim_Roar)) animator.Play(anim_Idle); // 조건이 맞으면 재생할거에요 -> 대기 애니를
|
||||
} // 코드 블록을 끝낼거에요 -> 대기 애니 처리 끝
|
||||
} // 코드 블록을 끝낼거에요 -> UpdateMovementAnimation를
|
||||
|
||||
private void LookAtTarget(Vector3 targetPos) // 함수를 정의할거에요 -> 타겟 바라보기를
|
||||
{ // 코드 블록을 시작할거에요 -> LookAtTarget 범위를
|
||||
Vector3 dir = targetPos - transform.position; // 계산할거에요 -> 방향 벡터를
|
||||
dir.y = 0f; // 무시할거에요 -> 높이 차이는
|
||||
|
||||
if (dir != Vector3.zero) // 조건이 맞으면 실행할거에요 -> 방향이 있다면
|
||||
{ // 코드 블록을 시작할거에요 -> 회전 처리 범위를
|
||||
transform.rotation = Quaternion.Slerp( // 회전할거에요 -> 부드럽게
|
||||
transform.rotation, // 현재 회전에서
|
||||
Quaternion.LookRotation(dir), // 목표 회전으로
|
||||
Time.deltaTime * 5f // 시간 속도에 맞춰서
|
||||
); // 회전 계산을 끝낼거에요
|
||||
} // 코드 블록을 끝낼거에요 -> 회전 처리 끝
|
||||
} // 코드 블록을 끝낼거에요 -> LookAtTarget를
|
||||
|
||||
private void DecideAttack() // 함수를 정의할거에요 -> 공격 패턴 결정을
|
||||
{ // 코드 블록을 시작할거에요 -> DecideAttack 범위를
|
||||
if (target != null) LookAtTarget(target.position); // 조건이 맞으면 바라볼거에요 -> 타겟이 있다면
|
||||
|
||||
string patternName = (counterSystem != null) ? counterSystem.SelectBossPattern() : "Normal"; // 선택할거에요 -> 패턴 이름을
|
||||
Debug.Log($"🧠 [Boss] 패턴 선택: {patternName}"); // 로그를 찍을거에요
|
||||
|
||||
switch (patternName) // 분기할거에요 -> 패턴에 따라
|
||||
{ // 코드 블록을 시작할거에요 -> switch 범위를
|
||||
case "DashAttack": StartCoroutine(Pattern_DashAttack()); break; // 실행할거에요 -> 돌진 공격을
|
||||
case "Smash": StartCoroutine(Pattern_SmashAttack()); break; // 실행할거에요 -> 찍기 공격을
|
||||
case "ShieldWall": StartCoroutine(Pattern_ShieldWall()); break; // 실행할거에요 -> 휩쓸기 공격을
|
||||
default: StartCoroutine(Pattern_ThrowBall()); break; // 실행할거에요 -> 공 던지기 공격을
|
||||
} // 코드 블록을 끝낼거에요 -> switch 끝
|
||||
} // 코드 블록을 끝낼거에요 -> DecideAttack를
|
||||
|
||||
private void RetrieveWeaponLogic() // 함수를 정의할거에요 -> 무기 회수 로직을
|
||||
{ // 코드 블록을 시작할거에요 -> RetrieveWeaponLogic 범위를
|
||||
if (ironBall == null || !agent.enabled || !agent.isOnNavMesh) return; // 중단할거에요 -> 무기나 메시 상태가 안 좋으면
|
||||
|
||||
agent.SetDestination(ironBall.transform.position); // 이동할거에요 -> 무기 위치로
|
||||
agent.isStopped = false; // 켤거에요 -> 이동을
|
||||
|
||||
UpdateMovementAnimation(); // 실행할거에요 -> 애니메이션 갱신을
|
||||
|
||||
if (Vector3.Distance(transform.position, ironBall.transform.position) <= 3.0f && !isAttacking) // 조건이 맞으면 실행할거에요 -> 가까워지면
|
||||
{ // 코드 블록을 시작할거에요 -> 줍기 코루틴 시작 범위를
|
||||
StartCoroutine(PickUpBallRoutine()); // 시작할거에요 -> 줍기 코루틴을
|
||||
} // 코드 블록을 끝낼거에요 -> 줍기 코루틴 시작 끝
|
||||
} // 코드 블록을 끝낼거에요 -> RetrieveWeaponLogic를
|
||||
|
||||
private IEnumerator PickUpBallRoutine() // 코루틴을 정의할거에요 -> 무기 줍기 과정을
|
||||
{ // 코드 블록을 시작할거에요 -> PickUpBallRoutine 범위를
|
||||
OnAttackStart(); // 실행할거에요 -> 공격 시작 설정을
|
||||
|
||||
if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 정상이라면
|
||||
{ // 코드 블록을 시작할거에요 -> 정지 처리 범위를
|
||||
agent.isStopped = true; // 정지시킬거에요 -> 에이전트를
|
||||
agent.velocity = Vector3.zero; // 멈출거에요 -> 속도를
|
||||
} // 코드 블록을 끝낼거에요 -> 정지 처리 끝
|
||||
|
||||
yield return StartCoroutine(PlayAndWait(anim_Pickup, 1.5f)); // 대기할거에요 -> 줍기 애니 동안
|
||||
|
||||
ironBall.transform.SetParent(handHolder); // 설정할거에요 -> 쇠공의 부모를 손으로
|
||||
ironBall.transform.localPosition = Vector3.zero; // 초기화할거에요 -> 상대 위치를
|
||||
ironBall.transform.localRotation = Quaternion.identity; // 초기화할거에요 -> 상대 회전을
|
||||
|
||||
if (ballRb != null) // 조건이 맞으면 실행할거에요 -> 쇠공 리지드바디가 있다면
|
||||
{ // 코드 블록을 시작할거에요 -> 쇠공 고정 범위를
|
||||
ballRb.isKinematic = true; // 설정할거에요 -> 쇠공 물리를 고정으로
|
||||
ballRb.velocity = Vector3.zero; // 멈출거에요 -> 속도를
|
||||
} // 코드 블록을 끝낼거에요 -> 쇠공 고정 끝
|
||||
|
||||
isWeaponless = false; // 설정할거에요 -> 무기 소지 상태로
|
||||
|
||||
OnAttackEnd(); // 실행할거에요 -> 공격 종료 설정을
|
||||
|
||||
if (agent != null && agent.isOnNavMesh) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 이동을 다시 켤거에요
|
||||
} // 코드 블록을 끝낼거에요 -> PickUpBallRoutine를
|
||||
|
||||
public override void OnAttackStart() // 함수를 정의할거에요 -> 공격 시작 시 공통 처리를
|
||||
{ // 코드 블록을 시작할거에요 -> OnAttackStart 범위를
|
||||
isAttacking = true; // 설정할거에요 -> 공격 중 상태를
|
||||
isPerformingAction = true; // 설정할거에요 -> 행동 수행 중 상태를
|
||||
if (agent != null) agent.isStopped = true; // 멈출거에요 -> 에이전트를
|
||||
} // 코드 블록을 끝낼거에요 -> OnAttackStart를
|
||||
|
||||
public override void OnAttackEnd() // 함수를 정의할거에요 -> 공격 종료 시 공통 처리를
|
||||
{ // 코드 블록을 시작할거에요 -> OnAttackEnd 범위를
|
||||
StartCoroutine(RecoverRoutine()); // 시작할거에요 -> 회복 코루틴을
|
||||
} // 코드 블록을 끝낼거에요 -> OnAttackEnd를
|
||||
|
||||
private IEnumerator RecoverRoutine() // 코루틴을 정의할거에요 -> 공격 후 휴식 과정을
|
||||
{ // 코드 블록을 시작할거에요 -> RecoverRoutine 범위를
|
||||
isAttacking = false; // 해제할거에요 -> 공격 중 상태를
|
||||
isResting = true; // 설정할거에요 -> 휴식 상태를
|
||||
|
||||
if (animator != null) animator.Play(anim_Idle); // 조건이 맞으면 재생할거에요 -> 대기 애니를
|
||||
|
||||
yield return new WaitForSeconds(patternInterval); // 기다릴거에요 -> 설정된 간격만큼
|
||||
|
||||
isResting = false; // 해제할거에요 -> 휴식 상태를
|
||||
isPerformingAction = false; // 해제할거에요 -> 행동 상태를
|
||||
} // 코드 블록을 끝낼거에요 -> RecoverRoutine를
|
||||
|
||||
private IEnumerator Pattern_DashAttack() // 코루틴을 정의할거에요 -> 돌진 공격 과정을
|
||||
{ // 코드 블록을 시작할거에요 -> Pattern_DashAttack 범위를
|
||||
OnAttackStart(); // 설정할거에요 -> 공격 시작
|
||||
|
||||
if (agent != null) agent.enabled = false; // 끌거에요 -> 에이전트를
|
||||
|
||||
yield return StartCoroutine(PlayAndWait(anim_DashReady, 0.5f)); // 대기할거에요 -> 준비 애니 동안
|
||||
|
||||
if (rb != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
|
||||
{ // 코드 블록을 시작할거에요 -> 돌진 물리 처리 범위를
|
||||
rb.isKinematic = false; // 설정할거에요 -> 물리를 켤거에요
|
||||
rb.velocity = transform.forward * 20f; // 날아갈거에요 -> 전방 속도로
|
||||
} // 코드 블록을 끝낼거에요 -> 돌진 물리 처리 끝
|
||||
|
||||
yield return StartCoroutine(PlayAndWait(anim_DashGo, 1.0f)); // 대기할거에요 -> 돌진 애니 동안
|
||||
|
||||
if (rb != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
|
||||
{ // 코드 블록을 시작할거에요 -> 정지 처리 범위를
|
||||
rb.velocity = Vector3.zero; // 멈출거에요 -> 속도를
|
||||
rb.isKinematic = true; // 설정할거에요 -> 물리를 다시 고정으로
|
||||
} // 코드 블록을 끝낼거에요 -> 정지 처리 끝
|
||||
|
||||
if (agent != null && isBattleStarted) agent.enabled = true; // 조건이 맞으면 실행할거에요 -> 에이전트를 다시 켤거에요
|
||||
|
||||
OnAttackEnd(); // 설정할거에요 -> 공격 종료
|
||||
} // 코드 블록을 끝낼거에요 -> Pattern_DashAttack를
|
||||
|
||||
private IEnumerator Pattern_SmashAttack() // 코루틴을 정의할거에요 -> 찍기 공격 과정을
|
||||
{ // 코드 블록을 시작할거에요 -> Pattern_SmashAttack 범위를
|
||||
OnAttackStart(); // 설정할거에요 -> 공격 시작
|
||||
yield return StartCoroutine(PlayAndWait(anim_SmashImpact, 1.0f)); // 대기할거에요 -> 타격 애니 동안
|
||||
OnAttackEnd(); // 설정할거에요 -> 공격 종료
|
||||
} // 코드 블록을 끝낼거에요 -> Pattern_SmashAttack를
|
||||
|
||||
private IEnumerator Pattern_ShieldWall() // 코루틴을 정의할거에요 -> 휩쓸기 공격 과정을
|
||||
{ // 코드 블록을 시작할거에요 -> Pattern_ShieldWall 범위를
|
||||
OnAttackStart(); // 설정할거에요 -> 공격 시작
|
||||
yield return StartCoroutine(PlayAndWait(anim_Sweep, 2.0f)); // 대기할거에요 -> 휩쓸기 애니 동안
|
||||
OnAttackEnd(); // 설정할거에요 -> 공격 종료
|
||||
} // 코드 블록을 끝낼거에요 -> Pattern_ShieldWall를
|
||||
|
||||
private IEnumerator Pattern_ThrowBall() // 코루틴을 정의할거에요 -> 공 던지기 과정을
|
||||
{ // 코드 블록을 시작할거에요 -> Pattern_ThrowBall 범위를
|
||||
OnAttackStart(); // 설정할거에요 -> 공격 시작
|
||||
|
||||
if (animator != null) animator.Play(anim_Throw, 0, 0f); // 재생할거에요 -> 던지기 애니를
|
||||
yield return null; // 1프레임 쉴거에요
|
||||
|
||||
float throwClipLength = GetClipLength(anim_Throw, 1.5f); // 가져올거에요 -> 클립 길이를
|
||||
float releaseTime = throwClipLength * 0.4f; // 계산할거에요 -> 발사 시점을
|
||||
|
||||
yield return new WaitForSeconds(Mathf.Max(0f, releaseTime - Time.deltaTime)); // 기다릴거에요 -> 발사 시점까지
|
||||
|
||||
if (ironBall != null && ballRb != null) // 조건이 맞으면 실행할거에요 -> 쇠공을 던질거에요
|
||||
{ // 코드 블록을 시작할거에요 -> 쇠공 발사 범위를
|
||||
ironBall.transform.SetParent(null); // 뺄거에요 -> 손에서
|
||||
ballRb.isKinematic = false; // 켤거에요 -> 물리를
|
||||
|
||||
Vector3 dir = (target.position - transform.position).normalized; // 계산할거에요 -> 방향을
|
||||
ballRb.AddForce(dir * 20f + Vector3.up * 5f, ForceMode.Impulse); // 던질거에요 -> 충격 힘으로
|
||||
ballRb.angularDrag = 5f; // 설정할거에요 -> 회전 저항을
|
||||
} // 코드 블록을 끝낼거에요 -> 쇠공 발사 끝
|
||||
|
||||
isWeaponless = true; // 설정할거에요 -> 무기 없음 상태를
|
||||
|
||||
float remainingTime = throwClipLength - releaseTime; // 계산할거에요 -> 남은 애니 시간을
|
||||
if (remainingTime > 0f) yield return new WaitForSeconds(remainingTime); // 조건이 맞으면 기다릴거에요 -> 남은 시간만큼
|
||||
|
||||
OnAttackEnd(); // 설정할거에요 -> 공격 종료
|
||||
} // 코드 블록을 끝낼거에요 -> Pattern_ThrowBall를
|
||||
|
||||
protected override void OnStartHit() // 함수를 정의할거에요 -> 피격 시 처리를
|
||||
{ // 코드 블록을 시작할거에요 -> OnStartHit 범위를
|
||||
// 보스는 피격되어도 행동이 끊기지 않습니다 (슈퍼아머) // 설명을 적을거에요 -> 의도적으로 비워둠
|
||||
} // 코드 블록을 끝낼거에요 -> OnStartHit를
|
||||
|
||||
protected override void OnDie() // 함수를 정의할거에요 -> 사망 시 처리를
|
||||
{ // 코드 블록을 시작할거에요 -> OnDie 범위를
|
||||
isBattleStarted = false; // 설정할거에요 -> 전투 종료
|
||||
isPerformingAction = false; // 해제할거에요 -> 행동 상태를
|
||||
if (bossHealthBar != null) bossHealthBar.SetActive(false); // 끌거에요 -> 체력바를
|
||||
GiveBossXP(); // 실행할거에요 -> 경험치 지급을
|
||||
base.OnDie(); // 부모의 사망 처리를 호출할거에요
|
||||
} // 코드 블록을 끝낼거에요 -> OnDie를
|
||||
|
||||
private void GiveBossXP() // 함수를 정의할거에요 -> 경험치 지급 기능을
|
||||
{ // 코드 블록을 시작할거에요 -> GiveBossXP 범위를
|
||||
Debug.Log("[BossController] 보스 처치 시도 중..."); // 로그를 찍을거에요 -> 지급 시도
|
||||
|
||||
if (ObsessionSystem.instance != null) // 조건이 맞으면 실행할거에요 -> 시스템 인스턴스가 있다면
|
||||
{ // 코드 블록을 시작할거에요 -> 지급 범위를
|
||||
ObsessionSystem.instance.AddRunXP(100); // 더할거에요 -> 경험치를 100만큼
|
||||
} // 코드 블록을 끝낼거에요 -> 지급 끝
|
||||
else // 조건이 틀리면 실행할거에요 -> 인스턴스가 없다면
|
||||
{ // 코드 블록을 시작할거에요 -> 에러 처리 범위를
|
||||
Debug.LogError("[BossController] ObsessionSystem 인스턴스를 찾을 수 없습니다!"); // 에러를 찍을거에요
|
||||
} // 코드 블록을 끝낼거에요 -> 에러 처리 끝
|
||||
|
||||
Debug.Log("<color=red>[BossController] 보스 처치 처리 완료!</color>"); // 로그를 찍을거에요 -> 빨간색으로
|
||||
} // 코드 블록을 끝낼거에요 -> GiveBossXP를
|
||||
|
||||
} // 코드 블록을 끝낼거에요 -> NorcielBoss를
|
||||
38
Assets/Scripts/Enemy/BossAI/BossWeapon.cs
Normal file
38
Assets/Scripts/Enemy/BossAI/BossWeapon.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
|
||||
public class BossWeapon : MonoBehaviour // 클래스를 선언할거에요 -> 보스 무기를
|
||||
{
|
||||
[Header("설정")] // 인스펙터 제목을 달거에요 -> 설정을
|
||||
[SerializeField] private float damage = 20f; // 변수를 선언할거에요 -> 데미지를
|
||||
[SerializeField] private string targetTag = "Player"; // 변수를 선언할거에요 -> 타겟 태그를
|
||||
|
||||
private Rigidbody rb; // 변수를 선언할거에요 -> 리지드바디를
|
||||
private Collider col; // 변수를 선언할거에요 -> 콜라이더를
|
||||
private bool isThrown; // 변수를 선언할거에요 -> 던져짐 여부를
|
||||
|
||||
private void Awake() // 함수를 실행할거에요 -> 초기화를
|
||||
{
|
||||
rb = GetComponent<Rigidbody>(); // 가져올거에요 -> 리지드바디를
|
||||
col = GetComponent<Collider>(); // 가져올거에요 -> 콜라이더를
|
||||
}
|
||||
|
||||
public void ThrowBall() { isThrown = true; if (col) col.isTrigger = true; } // 함수를 선언할거에요 -> 던짐 처리를 (통과)
|
||||
public void PickUpBall() { isThrown = false; if (col) col.isTrigger = true; } // 함수를 선언할거에요 -> 줍기 처리를 (통과)
|
||||
|
||||
private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 트리거 충돌 시
|
||||
{
|
||||
if (isThrown && other.CompareTag(targetTag)) ApplyDamage(other.gameObject); // 조건이 맞으면 실행할거에요 -> 플레이어 피격을
|
||||
else if (isThrown && other.CompareTag("Ground")) if (col) col.isTrigger = false; // 조건이 맞으면 실행할거에요 -> 바닥이면 물리로 전환을
|
||||
}
|
||||
|
||||
private void OnCollisionEnter(Collision col) // 함수를 실행할거에요 -> 물리 충돌 시
|
||||
{
|
||||
if (isThrown && col.gameObject.CompareTag(targetTag) && rb.velocity.magnitude > 2f) ApplyDamage(col.gameObject); // 조건이 맞으면 실행할거에요 -> 굴러서 부딪히면 데미지를
|
||||
}
|
||||
|
||||
private void ApplyDamage(GameObject target) // 함수를 선언할거에요 -> 데미지 적용을
|
||||
{
|
||||
var hp = target.GetComponent<IDamageable>(); // 가져올거에요 -> 체력 스크립트를
|
||||
if (hp != null) hp.TakeDamage(damage); // 실행할거에요 -> 데미지 함수를
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemy/BossAI/BossWeapon.cs.meta
Normal file
11
Assets/Scripts/Enemy/BossAI/BossWeapon.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8aab89a77c422e841ab70b85a3a041eb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,29 +1,27 @@
|
|||
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
|
||||
public class BossZoneTrigger : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossZoneTrigger를
|
||||
public class BossZoneTrigger : MonoBehaviour // 클래스를 선언할거에요 -> 보스방 입장 트리거를
|
||||
{
|
||||
[SerializeField] private NorcielBoss boss; // 변수를 선언할거에요 -> 깨울 보스 스크립트(NorcielBoss)를 boss에
|
||||
[SerializeField] private GameObject fogWall; // 변수를 선언할거에요 -> 입구를 막을 안개벽 오브젝트를 fogWall에
|
||||
[SerializeField] private NorcielBoss boss; // 변수를 선언할거에요 -> 보스 참조를
|
||||
[SerializeField] private GameObject entranceWall; // 변수를 선언할거에요 -> 입구 막는 벽을
|
||||
|
||||
private bool hasTriggered = false; // 변수를 초기화할거에요 -> 이미 발동했는지 여부를 거짓(false)으로
|
||||
private bool isTriggered = false; // 변수를 선언할거에요 -> 이미 발동했는지를
|
||||
|
||||
private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 무언가가 트리거에 닿았을 때 OnTriggerEnter를
|
||||
private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 충돌 감지를
|
||||
{
|
||||
// 이미 발동했거나, 플레이어가 아니면 무시
|
||||
if (hasTriggered) return; // 조건이 맞으면 중단할거에요 -> 이미 발동했다면(hasTriggered가 참이면)
|
||||
if (isTriggered) return; // 중단할거에요 -> 이미 발동했으면
|
||||
|
||||
if (other.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 닿은 물체의 태그가 "Player"라면
|
||||
if (other.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 플레이어가 들어오면
|
||||
{
|
||||
hasTriggered = true; // 상태를 바꿀거에요 -> 발동 상태를 참(true)으로
|
||||
isTriggered = true; // 설정할거에요 -> 발동됨으로
|
||||
if (entranceWall != null) entranceWall.SetActive(true); // 켤거에요 -> 벽을 (못 나가게)
|
||||
|
||||
// 1. 보스 깨우기
|
||||
if (boss != null) boss.StartBossBattle(); // 조건이 맞으면 실행할거에요 -> 보스가 연결되어 있다면 전투 시작 함수(StartBossBattle)를
|
||||
if (boss != null) // 조건이 맞으면 실행할거에요 -> 보스가 연결돼있으면
|
||||
{
|
||||
boss.StartBossBattle(); // 실행할거에요 -> 보스전 시작을
|
||||
}
|
||||
|
||||
// 2. 도망 못 가게 입구 막기 (선택)
|
||||
if (fogWall != null) fogWall.SetActive(true); // 조건이 맞으면 실행할거에요 -> 안개벽이 연결되어 있다면 활성화(true)를
|
||||
|
||||
// 3. 이 트리거는 할 일 다 했으니 끄기
|
||||
// gameObject.SetActive(false); // 바로 끄면 안개도 꺼질 수 있으니 주의
|
||||
Debug.Log("⚔️ 보스전 시작! 퇴로는 없다."); // 로그를 찍을거에요 -> 시작 알림을
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
public enum CounterType // 열거형을 선언할거에요 -> CounterType이라는 이름의 enum을
|
||||
{ // 코드 블록을 시작할거에요 -> CounterType 범위를
|
||||
None, // 값을 정의할거에요 -> 카운터 없음 상태를
|
||||
Dodge, // 값을 정의할거에요 -> 회피 남발 카운터 타입을
|
||||
Aim, // 값을 정의할거에요 -> 정조준 과다 카운터 타입을
|
||||
Pierce // 값을 정의할거에요 -> 관통 화살 편중 카운터 타입을
|
||||
KeepDistance, // 값을 정의할거에요 -> 플레이어가 멀리 있을 때 (N m 이상)를
|
||||
CloseRange, // 값을 정의할거에요 -> 플레이어가 근접해 있을 때 (N m 이하)를
|
||||
FrequentDash // 값을 정의할거에요 -> 플레이어가 대쉬를 많이 사용할 때를
|
||||
} // 코드 블록을 끝낼거에요 -> CounterType을
|
||||
55
Assets/Scripts/Enemy/BossAI/PatternSelector.cs
Normal file
55
Assets/Scripts/Enemy/BossAI/PatternSelector.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
using System.Collections.Generic; // 딕셔너리를 사용할거에요 -> System.Collections.Generic을
|
||||
|
||||
public class PatternSelector // 클래스를 선언할거에요 -> 패턴 선택 로직을
|
||||
{
|
||||
private readonly BossCounterConfig _config; // 변수를 선언할거에요 -> 설정 참조를
|
||||
|
||||
public PatternSelector(BossCounterConfig config) // 생성자를 만들거에요 -> 설정을 주입받는
|
||||
{
|
||||
_config = config; // 값을 저장할거에요 -> 내부 변수에
|
||||
}
|
||||
|
||||
public string SelectPattern(Dictionary<CounterType, int> counters) // 함수를 선언할거에요 -> 패턴 선택을
|
||||
{
|
||||
if (!_config) return "Normal"; // 반환할거에요 -> 설정 없으면 기본값을
|
||||
|
||||
float totalWeight = 0f; // 변수를 초기화할거에요 -> 총 가중치를
|
||||
var weights = new Dictionary<string, float>(); // 딕셔너리를 만들거에요 -> 패턴별 확률을
|
||||
|
||||
foreach (var p in _config.patterns) // 반복할거에요 -> 모든 패턴 설정을
|
||||
{
|
||||
float w = p.baseWeight; // 값을 가져올거에요 -> 기본 가중치를
|
||||
|
||||
// 카운터가 있으면 가중치 증가
|
||||
if (counters.TryGetValue(p.targetCounter, out int count))
|
||||
w += count * p.weightMultiplier;
|
||||
|
||||
// ⭐ [핵심 수정] 중복 키 에러 방지 및 가중치 합산
|
||||
if (weights.ContainsKey(p.patternName)) // 조건이 맞으면 실행할거에요 -> 이미 같은 패턴 이름이 있다면
|
||||
{
|
||||
weights[p.patternName] += w; // 더할거에요 -> 기존 확률에 합산 (조건 A or 조건 B)
|
||||
}
|
||||
else // 조건이 틀리면 실행할거에요 -> 처음 나온 패턴이라면
|
||||
{
|
||||
weights[p.patternName] = w; // 저장할거에요 -> 새 항목으로
|
||||
}
|
||||
|
||||
totalWeight += w; // 더할거에요 -> 총합에 (이 부분 주의: 중복 합산 시 totalWeight 계산 로직 수정 필요)
|
||||
}
|
||||
|
||||
// totalWeight 재계산 (중복 합산된 최종 값으로)
|
||||
totalWeight = 0f;
|
||||
foreach (var val in weights.Values) totalWeight += val;
|
||||
|
||||
float rnd = Random.Range(0, totalWeight); // 값을 뽑을거에요 -> 랜덤으로
|
||||
float sum = 0; // 변수를 초기화할거에요 -> 누적합을
|
||||
|
||||
foreach (var pair in weights) // 반복할거에요 -> 각 패턴을
|
||||
{
|
||||
sum += pair.Value; // 더할거에요 -> 확률을
|
||||
if (rnd <= sum) return pair.Key; // 반환할거에요 -> 당첨된 패턴을
|
||||
}
|
||||
return "Normal"; // 반환할거에요 -> 꽝이면 기본값을
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemy/BossAI/PatternSelector.cs.meta
Normal file
11
Assets/Scripts/Enemy/BossAI/PatternSelector.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5f836bfcc212dd843a4c9ae7b2353d0d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,163 +1,67 @@
|
|||
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
|
||||
using System.Collections.Generic; // 리스트를 사용할거에요 -> System.Collections.Generic을
|
||||
|
||||
/// <summary>
|
||||
/// 하이브리드 몬스터 스포너 (생성은 스포너 기준, 생존은 몬스터 기준)
|
||||
/// - Birth: 스포너 기준 거리로 생성
|
||||
/// - Life & Hibernate: 몬스터 기준 거리로 최적화
|
||||
/// - Wake Up: 범위 안으로 복귀 시 재활성화
|
||||
/// </summary>
|
||||
public class MonsterSpawner : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 MonsterSpawner를
|
||||
public class MonsterSpwaner : MonoBehaviour // 클래스를 선언할거에요 -> 몬스터 스포너를
|
||||
{
|
||||
[Header("=== 생성 설정 (Birth - 스포너 기준) ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 생성 설정 (Birth - 스포너 기준) === 을
|
||||
[Tooltip("스포너로부터 이 거리 안에 플레이어가 들어오면 몬스터 생성")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
[SerializeField] private float spawnRange = 25f; // 변수를 선언할거에요 -> 몬스터 생성 거리(25.0)를 spawnRange에
|
||||
[Header("스폰 설정")] // 인스펙터 제목을 달거에요 -> 스폰 설정을
|
||||
[SerializeField] private List<GameObject> monsterPrefabs; // 리스트를 선언할거에요 -> 몬스터 프리팹들을
|
||||
[SerializeField] private List<Transform> spawnPoints; // 리스트를 선언할거에요 -> 스폰 위치들을
|
||||
[SerializeField] private float spawnInterval = 5f; // 변수를 선언할거에요 -> 스폰 간격을
|
||||
[SerializeField] private int maxMonsters = 20; // 변수를 선언할거에요 -> 최대 몬스터 수를
|
||||
|
||||
[Tooltip("몬스터가 죽은 후 재생성까지 대기 시간")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
[SerializeField] private float respawnCooldown = 3f; // 변수를 선언할거에요 -> 재소환 대기시간(3초)을 respawnCooldown에
|
||||
// 오브젝트 풀 (선택 사항)
|
||||
// private GenericObjectPool pool; // 나중에 풀링 시스템과 연동 가능
|
||||
|
||||
[Tooltip("오브젝트 풀에서 가져올 몬스터 태그")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
[SerializeField] private string mobTag = "NormalMob"; // 변수를 선언할거에요 -> 소환할 몬스터의 태그 이름("NormalMob")을 mobTag에
|
||||
private int currentMonsterCount = 0; // 변수를 선언할거에요 -> 현재 몬스터 수를
|
||||
|
||||
[Header("=== 최적화 설정 (Life & Hibernate - 몬스터 기준) ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 최적화 설정 (Life & Hibernate - 몬스터 기준) === 을
|
||||
[Tooltip("몬스터 본체로부터 이 거리를 벗어나면 강제 동면 (전투 중이어도!)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
[SerializeField] private float optimizationRange = 60f; // 변수를 선언할거에요 -> 몬스터 최적화(동면) 거리(60.0)를 optimizationRange에
|
||||
|
||||
// 내부 상태
|
||||
private GameObject _myMonster; // 변수를 선언할거에요 -> 소환된 몬스터 게임오브젝트를 담을 _myMonster를
|
||||
private MonsterClass _monsterScript; // 변수를 선언할거에요 -> 몬스터의 스크립트(MonsterClass)를 담을 _monsterScript를
|
||||
private Transform _player; // 변수를 선언할거에요 -> 플레이어의 위치 정보를 담을 _player를
|
||||
private float _nextSpawnTime; // 변수를 선언할거에요 -> 다음 소환 가능 시간을 저장할 _nextSpawnTime을
|
||||
|
||||
private void Start() // 함수를 실행할거에요 -> 게임 시작 시 호출되는 Start를
|
||||
private void Start() // 함수를 실행할거에요 -> 시작 Start를
|
||||
{
|
||||
FindPlayer(); // 함수를 실행할거에요 -> 플레이어를 찾는 FindPlayer를
|
||||
StartCoroutine(SpawnRoutine()); // 실행할거에요 -> 스폰 코루틴을
|
||||
MonsterClass.OnMonsterKilled += HandleMonsterDeath; // 구독할거에요 -> 몬스터 사망 이벤트를
|
||||
}
|
||||
|
||||
private void FindPlayer() // 함수를 선언할거에요 -> 플레이어를 찾아 변수에 저장하는 FindPlayer를
|
||||
private void OnDestroy() // 함수를 실행할거에요 -> 파괴될 때
|
||||
{
|
||||
GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); // 오브젝트를 찾을거에요 -> "Player" 태그가 붙은 오브젝트를 playerObj에
|
||||
if (playerObj != null) _player = playerObj.transform; // 조건이 맞으면 실행할거에요 -> 플레이어를 찾았다면 그 위치 정보를 _player에 저장
|
||||
MonsterClass.OnMonsterKilled -= HandleMonsterDeath; // 해지할거에요 -> 이벤트 구독을
|
||||
}
|
||||
|
||||
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를
|
||||
private void HandleMonsterDeath(int exp) // 함수를 선언할거에요 -> 사망 처리 핸들러를
|
||||
{
|
||||
if (_player == null) { FindPlayer(); return; } // 조건이 맞으면 실행할거에요 -> 플레이어 정보가 없다면 다시 찾고 이번 프레임은 중단(return)
|
||||
|
||||
// 몬스터가 없거나 죽었으면 -> Birth
|
||||
if (_myMonster == null || (_monsterScript != null && _monsterScript.IsDead)) // 조건이 맞으면 실행할거에요 -> 몬스터가 없거나, 몬스터 스크립트가 있는데 죽은 상태라면
|
||||
{
|
||||
HandleBirth(); // 함수를 실행할거에요 -> 몬스터 생성 로직인 HandleBirth를
|
||||
}
|
||||
// 몬스터가 살아있으면 -> Life & Hibernate
|
||||
else // 조건이 틀리면 실행할거에요 -> 몬스터가 살아서 활동 중이라면
|
||||
{
|
||||
HandleLifeAndHibernate(); // 함수를 실행할거에요 -> 생존 및 최적화(동면) 로직인 HandleLifeAndHibernate를
|
||||
}
|
||||
currentMonsterCount--; // 줄일거에요 -> 현재 몬스터 수를
|
||||
if (currentMonsterCount < 0) currentMonsterCount = 0; // 보정할거에요 -> 0 밑으로 안 가게
|
||||
}
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// 🐣 BIRTH (생성) - 스포너 기준
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
private void HandleBirth() // 함수를 선언할거에요 -> 몬스터 생성 조건을 확인하는 HandleBirth를
|
||||
private IEnumerator SpawnRoutine() // 코루틴 함수를 정의할거에요 -> 반복 스폰 로직을
|
||||
{
|
||||
// ⭐ 스포너 <-> 플레이어 거리
|
||||
float distanceFromSpawner = Vector3.Distance(transform.position, _player.position); // 값을 계산할거에요 -> 스포너와 플레이어 사이의 거리를 distanceFromSpawner에
|
||||
|
||||
if (distanceFromSpawner <= spawnRange && Time.time >= _nextSpawnTime) // 조건이 맞으면 실행할거에요 -> 거리가 생성 범위 이내이고, 쿨타임 시간이 지났다면
|
||||
while (true) // 반복할거에요 -> 무한히
|
||||
{
|
||||
SpawnMonster(); // 함수를 실행할거에요 -> 실제 몬스터를 소환하는 SpawnMonster를
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnMonster() // 함수를 선언할거에요 -> 오브젝트 풀에서 몬스터를 가져오는 SpawnMonster를
|
||||
{
|
||||
_myMonster = GenericObjectPool.Instance.SpawnFromPool(mobTag, transform.position, transform.rotation); // 오브젝트를 가져올거에요 -> 풀(Pool)에서 mobTag에 맞는 몬스터를 스포너 위치에 소환해서 _myMonster에
|
||||
|
||||
if (_myMonster != null) // 조건이 맞으면 실행할거에요 -> 몬스터 소환에 성공했다면
|
||||
{
|
||||
_monsterScript = _myMonster.GetComponent<MonsterClass>(); // 컴포넌트를 가져올거에요 -> 몬스터의 핵심 스크립트(MonsterClass)를 _monsterScript에
|
||||
if (_monsterScript != null) _monsterScript.ResetStats(); // 조건이 맞으면 실행할거에요 -> 스크립트가 있다면 몬스터의 상태(체력 등)를 초기화(ResetStats)
|
||||
}
|
||||
}
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// 💤 LIFE & HIBERNATE (생존/동면) - 몬스터 기준
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
private void HandleLifeAndHibernate() // 함수를 선언할거에요 -> 몬스터의 활동 및 최적화를 관리하는 HandleLifeAndHibernate를
|
||||
{
|
||||
if (_myMonster == null) { _monsterScript = null; return; } // 조건이 맞으면 실행할거에요 -> 몬스터 오브젝트가 사라졌다면 스크립트 변수도 비우고 중단(return)
|
||||
|
||||
// ⭐ 몬스터 본체 <-> 플레이어 거리
|
||||
float distanceFromMonster = Vector3.Distance(_myMonster.transform.position, _player.position); // 값을 계산할거에요 -> 몬스터와 플레이어 사이의 거리를 distanceFromMonster에
|
||||
|
||||
if (distanceFromMonster > optimizationRange) // 조건이 맞으면 실행할거에요 -> 몬스터와 플레이어 거리가 최적화 범위(optimizationRange)보다 멀다면
|
||||
{
|
||||
// 😴 HIBERNATE (동면)
|
||||
HibernateMonster(); // 함수를 실행할거에요 -> 몬스터를 잠재우는 HibernateMonster를
|
||||
}
|
||||
else // 조건이 틀리면 실행할거에요 -> 거리가 가깝다면
|
||||
{
|
||||
// 👁 WAKE UP (기상)
|
||||
WakeUpMonster(); // 함수를 실행할거에요 -> 몬스터를 깨우는 WakeUpMonster를
|
||||
}
|
||||
}
|
||||
|
||||
private void HibernateMonster() // 함수를 선언할거에요 -> 몬스터를 비활성화하는 HibernateMonster를
|
||||
{
|
||||
if (_myMonster.activeSelf) // 조건이 맞으면 실행할거에요 -> 몬스터가 현재 켜져 있는(Active) 상태라면
|
||||
{
|
||||
_myMonster.SetActive(false); // 기능을 끌거에요 -> 몬스터 오브젝트를 비활성화(false)로
|
||||
}
|
||||
}
|
||||
|
||||
private void WakeUpMonster() // 함수를 선언할거에요 -> 몬스터를 다시 활성화하는 WakeUpMonster를
|
||||
{
|
||||
if (!_myMonster.activeSelf) // 조건이 맞으면 실행할거에요 -> 몬스터가 현재 꺼져 있는(Inactive) 상태라면
|
||||
{
|
||||
_myMonster.SetActive(true); // 기능을 켤거에요 -> 몬스터 오브젝트를 활성화(true)로
|
||||
|
||||
if (_monsterScript != null) // 조건이 맞으면 실행할거에요 -> 몬스터 스크립트가 존재한다면
|
||||
if (currentMonsterCount < maxMonsters) // 조건이 맞으면 실행할거에요 -> 꽉 차지 않았다면
|
||||
{
|
||||
// ⭐ AI 복구 (전투 중이었다면 추적 재개)
|
||||
_monsterScript.Reactivate(); // 함수를 실행할거에요 -> 몬스터의 AI를 다시 작동시키는 Reactivate를
|
||||
SpawnMonster(); // 실행할거에요 -> 스폰 함수를
|
||||
}
|
||||
yield return new WaitForSeconds(spawnInterval); // 기다릴거에요 -> 간격만큼
|
||||
}
|
||||
}
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// 💀 DEATH (사망 처리 및 쿨타임)
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
/// <summary>
|
||||
/// 몬스터가 죽었을 때 호출 (쿨타임 적용)
|
||||
/// </summary>
|
||||
public void NotifyMonsterDead() // 함수를 선언할거에요 -> 외부에서 몬스터 사망을 알릴 때 쓰는 NotifyMonsterDead를
|
||||
private void SpawnMonster() // 함수를 선언할거에요 -> 실제 생성 로직을
|
||||
{
|
||||
// 쿨타임 계산: 현재 시간 + 대기 시간
|
||||
_nextSpawnTime = Time.time + respawnCooldown; // 값을 저장할거에요 -> 현재 시간에 쿨타임을 더해서 다음 소환 시간(_nextSpawnTime)으로
|
||||
if (monsterPrefabs.Count == 0 || spawnPoints.Count == 0) return; // 중단할거에요 -> 설정이 없으면
|
||||
|
||||
// 몬스터 참조를 비워서 Update에서 다시 HandleBirth()로 넘어가게 함
|
||||
_myMonster = null; // 값을 비울거에요 -> 소환된 몬스터 변수를 null로
|
||||
_monsterScript = null; // 값을 비울거에요 -> 몬스터 스크립트 변수를 null로
|
||||
}
|
||||
// 랜덤 선택
|
||||
GameObject prefab = monsterPrefabs[Random.Range(0, monsterPrefabs.Count)]; // 뽑을거에요 -> 랜덤 몬스터를
|
||||
Transform point = spawnPoints[Random.Range(0, spawnPoints.Count)]; // 뽑을거에요 -> 랜덤 위치를
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// Gizmos
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// 생성 (풀링을 쓴다면 pool.Get() 사용)
|
||||
GameObject mob = Instantiate(prefab, point.position, point.rotation); // 생성할거에요 -> 몬스터를
|
||||
|
||||
private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 에디터에서 오브젝트 선택 시 기즈모를 그리는 OnDrawGizmosSelected를
|
||||
{
|
||||
// 스포너 생성 범위 (빨간색)
|
||||
Gizmos.color = Color.red; // 색상을 설정할거에요 -> 기즈모 색을 빨간색으로
|
||||
Gizmos.DrawWireSphere(transform.position, spawnRange); // 그림을 그릴거에요 -> 스포너 위치에 생성 범위(spawnRange) 크기의 원을
|
||||
|
||||
// 몬스터 최적화 범위 (파란색)
|
||||
if (_myMonster != null) // 조건이 맞으면 실행할거에요 -> 소환된 몬스터가 있다면
|
||||
// 초기화 확인
|
||||
if (mob.TryGetComponent<MonsterClass>(out var monsterScript)) // 가져올거에요 -> 몬스터 스크립트를
|
||||
{
|
||||
Gizmos.color = Color.blue; // 색상을 설정할거에요 -> 기즈모 색을 파란색으로
|
||||
Gizmos.DrawWireSphere(_myMonster.transform.position, optimizationRange); // 그림을 그릴거에요 -> 몬스터 위치에 최적화 범위(optimizationRange) 크기의 원을
|
||||
monsterScript.ResetStats(); // 실행할거에요 -> 스탯 초기화를
|
||||
monsterScript.Reactivate(); // 실행할거에요 -> 활성화를
|
||||
}
|
||||
|
||||
currentMonsterCount++; // 늘릴거에요 -> 카운트를
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +1,50 @@
|
|||
using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
|
||||
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
using System.Collections.Generic; // 리스트를 사용할거에요 -> 제네릭을
|
||||
|
||||
public class MobUpdateManager : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 MobUpdateManager를
|
||||
/// <summary>
|
||||
/// 모든 몬스터의 Update를 중앙에서 관리하여 성능을 최적화하는 매니저
|
||||
/// </summary>
|
||||
public class MobUpdateManager : MonoBehaviour // 클래스를 선언할거에요 -> 몹 업데이트 매니저를
|
||||
{
|
||||
public static MobUpdateManager Instance; // 변수를 선언할거에요 -> 어디서든 접근 가능한 싱글톤 인스턴스 Instance를
|
||||
// ⭐ Monster -> MonsterClass로 수정하여 에러 해결
|
||||
private List<MonsterClass> _activeMobs = new List<MonsterClass>(); // 리스트를 초기화할거에요 -> 활성화된 몬스터들을 담을 _activeMobs를
|
||||
public static MobUpdateManager Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스를
|
||||
|
||||
private void Awake() => Instance = this; // 함수를 실행할거에요 -> 스크립트 시작 시 내 자신(this)을 Instance에 넣기를
|
||||
private readonly List<MonsterClass> _activeMobs = new List<MonsterClass>(); // 리스트를 선언할거에요 -> 활성 몬스터 목록을
|
||||
|
||||
// ⭐ 매개변수 타입도 MonsterClass로 변경
|
||||
public void RegisterMob(MonsterClass mob) => _activeMobs.Add(mob); // 함수를 선언할거에요 -> 몬스터를 리스트에 추가하는 RegisterMob을
|
||||
public void UnregisterMob(MonsterClass mob) => _activeMobs.Remove(mob); // 함수를 선언할거에요 -> 몬스터를 리스트에서 제거하는 UnregisterMob을
|
||||
|
||||
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를
|
||||
private void Awake() // 함수를 실행할거에요 -> 초기화 Awake를
|
||||
{
|
||||
for (int i = 0; i < _activeMobs.Count; i++) // 반복할거에요 -> 리스트에 있는 모든 몬스터 개수만큼
|
||||
if (Instance == null) Instance = this; // 조건이 맞으면 설정할거에요 -> 나를 유일한 인스턴스로
|
||||
else Destroy(gameObject); // 아니면 파괴할거에요 -> 중복된 나를
|
||||
}
|
||||
|
||||
public void RegisterMob(MonsterClass mob) // 함수를 선언할거에요 -> 몬스터 등록을
|
||||
{
|
||||
if (!_activeMobs.Contains(mob)) // 조건이 맞으면 실행할거에요 -> 목록에 없다면
|
||||
{
|
||||
// 리스트를 돌며 카메라 최적화 로직이 담긴 OnManagedUpdate 호출
|
||||
if (_activeMobs[i] != null) _activeMobs[i].OnManagedUpdate(); // 조건이 맞으면 실행할거에요 -> 몬스터가 존재한다면 관리 업데이트 함수(OnManagedUpdate)를
|
||||
_activeMobs.Add(mob); // 추가할거에요 -> 목록에
|
||||
}
|
||||
}
|
||||
|
||||
public void UnregisterMob(MonsterClass mob) // 함수를 선언할거에요 -> 몬스터 해제를
|
||||
{
|
||||
if (_activeMobs.Contains(mob)) // 조건이 맞으면 실행할거에요 -> 목록에 있다면
|
||||
{
|
||||
_activeMobs.Remove(mob); // 제거할거에요 -> 목록에서
|
||||
}
|
||||
}
|
||||
|
||||
private void Update() // 함수를 실행할거에요 -> 매 프레임마다
|
||||
{
|
||||
// 역순 반복문: 중간에 삭제되어도 안전하게
|
||||
for (int i = _activeMobs.Count - 1; i >= 0; i--) // 반복할거에요 -> 뒤에서부터 하나씩
|
||||
{
|
||||
if (_activeMobs[i] != null && _activeMobs[i].gameObject.activeInHierarchy) // 조건이 맞으면 실행할거에요 -> 몬스터가 켜져 있다면
|
||||
{
|
||||
_activeMobs[i].OnManagedUpdate(); // 실행할거에요 -> 관리형 업데이트 함수를
|
||||
}
|
||||
else // 꺼져있거나 삭제됐다면
|
||||
{
|
||||
_activeMobs.RemoveAt(i); // 제거할거에요 -> 리스트에서 정리
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +1,15 @@
|
|||
using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
|
||||
using System; // Action 이벤트를 쓰기 위해 불러올거에요 -> System을
|
||||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
|
||||
public class TrainingDummy : MonoBehaviour, IDamageable // 클래스를 선언할거에요 -> 더미 오브젝트가 데미지를 받게 만들 TrainingDummy를
|
||||
{ // 코드 블록을 시작할거에요 -> TrainingDummy 범위를
|
||||
public class DummyBot : MonsterClass // 클래스를 선언할거에요 -> 샌드백 몬스터를
|
||||
{
|
||||
// 더미는 아무것도 안 함
|
||||
protected override void Init() { } // 초기화할거에요 -> 빈 상태로
|
||||
protected override void ExecuteAILogic() { } // 실행할거에요 -> 아무 로직도 없이 (가만히 있음)
|
||||
protected override void OnResetStats() { } // 리셋할거에요 -> 아무것도 안 함
|
||||
|
||||
[Header("더미 설정")] // 인스펙터에 제목을 표시할거에요 -> 더미 설정을
|
||||
[SerializeField] private float maxHP = 9999f; // 변수를 선언할거에요 -> 최대 체력을 9999로 설정하는 maxHP를
|
||||
private float _currentHP; // 변수를 선언할거에요 -> 현재 체력을 저장할 _currentHP를
|
||||
|
||||
private Animator _animator; // 변수를 선언할거에요 -> 애니메이터 컴포넌트를 담을 _animator를
|
||||
|
||||
// ⭐ UI가 이 신호를 듣고 게이지를 줄입니다. // 설명을 적을거에요 -> UI 업데이트용 이벤트임을
|
||||
public event Action<float, float> OnHealthChanged; // 이벤트를 선언할거에요 -> (현재체력, 최대체력) 알림을 보내는 OnHealthChanged를
|
||||
|
||||
private void Start() // 함수를 선언할거에요 -> 시작할 때 1번 실행되는 Start를
|
||||
{ // 코드 블록을 시작할거에요 -> Start 범위를
|
||||
_currentHP = maxHP; // 값을 넣을거에요 -> 현재 체력을 최대 체력으로 초기화
|
||||
_animator = GetComponent<Animator>(); // 컴포넌트를 가져올거에요 -> Animator를 찾아 _animator에
|
||||
} // 코드 블록을 끝낼거에요 -> Start를
|
||||
|
||||
public void TakeDamage(float amount) // 함수를 선언할거에요 -> 외부에서 데미지를 주면 호출되는 TakeDamage를
|
||||
{ // 코드 블록을 시작할거에요 -> TakeDamage 범위를
|
||||
_currentHP -= amount; // 값을 뺄거에요 -> 현재 체력에서 데미지만큼 감소
|
||||
|
||||
// ⭐ UI에 현재 체력과 최대 체력 정보를 보냅니다. // 설명을 적을거에요 -> UI 갱신 트리거임을
|
||||
OnHealthChanged?.Invoke(_currentHP, maxHP); // 이벤트를 호출할거에요 -> UI에게 체력 변화 전달
|
||||
|
||||
Debug.Log($"<color=red>[더미]</color> 피격! 데미지: {amount} | 남은 체력: {_currentHP}"); // 로그를 찍을거에요 -> 데미지/남은 체력 확인용
|
||||
|
||||
if (_animator != null) // 조건을 검사할거에요 -> 애니메이터가 있는지
|
||||
{ // 코드 블록을 시작할거에요 -> 애니메이션 재생 처리
|
||||
_animator.Play("Hit", 0, 0f); // 애니메이션을 재생할거에요 -> Hit 상태를 0번 레이어에서 처음부터
|
||||
} // 코드 블록을 끝낼거에요 -> 애니메이션 처리
|
||||
} // 코드 블록을 끝낼거에요 -> TakeDamage를
|
||||
|
||||
} // 코드 블록을 끝낼거에요 -> TrainingDummy를
|
||||
public override void TakeDamage(float amount) // 함수를 덮어씌워 실행할거에요 -> 피격 처리를
|
||||
{
|
||||
base.TakeDamage(amount); // 실행할거에요 -> 기본 피격 처리를
|
||||
Debug.Log($"🥊 샌드백 맞음! 남은 체력: {currentHP}"); // 로그를 찍을거에요 -> 데미지 확인용
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
%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: 127d57ae132759d4f8233580c8075e39, type: 3}
|
||||
m_Name: BossCounterConfig
|
||||
m_EditorClassIdentifier:
|
||||
dodgeThreshold: 5
|
||||
aimThreshold: 4
|
||||
pierceThreshold: 0.6
|
||||
minShotsForPierceCheck: 3
|
||||
unlockedThresholdMultiplier: 0.8
|
||||
counterWeightBonus: 3
|
||||
counterSubWeightBonus: 2
|
||||
counterDecayTime: 8
|
||||
maxCounterFrequency: 0.25
|
||||
counterCooldown: 5
|
||||
habitChangeRewardRatio: 0.15
|
||||
|
|
@ -1,145 +1,180 @@
|
|||
using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
|
||||
using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
|
||||
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||
|
||||
/// <summary>
|
||||
/// 플레이어의 전투 행동을 슬라이딩 윈도우(최근 N초) 기반으로 추적합니다.
|
||||
/// 플레이어의 전투 행동을 추적하고 보스 카운터 시스템에 보고합니다.
|
||||
/// </summary>
|
||||
public class PlayerBehaviorTracker : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerBehaviorTracker를
|
||||
{
|
||||
public static PlayerBehaviorTracker Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스 접근용 Instance를
|
||||
|
||||
[Header("슬라이딩 윈도우 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 슬라이딩 윈도우 설정 을
|
||||
[Tooltip("행동 추적 윈도우 크기(초)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||
[SerializeField] private float windowDuration = 10f; // 변수를 선언할거에요 -> 추적 시간 범위(10초)를 windowDuration에
|
||||
[Header("참조")] // 인스펙터 제목을 달거에요 -> 참조 설정을
|
||||
[SerializeField] private Transform bossTransform; // 변수를 선언할거에요 -> 보스의 위치를 파악할 bossTransform을
|
||||
private BossCounterSystem _bossSystem; // 변수를 선언할거에요 -> 보스 카운터 시스템 참조를
|
||||
|
||||
[Header("행동 추적 설정")] // 인스펙터 제목을 달거에요 -> 추적 설정을
|
||||
[Tooltip("행동 추적 윈도우 크기(초)")] // 툴팁을 달거에요 -> 설명 문구를
|
||||
[SerializeField] private float windowDuration = 10f; // 변수를 선언할거에요 -> 기록 유지 시간(10초)을
|
||||
|
||||
[Header("조건 설정")] // 인스펙터 제목을 달거에요 -> 조건 설정을
|
||||
[SerializeField] private float keepDistanceThreshold = 8f; // 변수를 선언할거에요 -> 거리 유지 기준(8m)을
|
||||
[SerializeField] private float keepDistanceTimeReq = 3f; // 변수를 선언할거에요 -> 거리 유지 필요 시간(3초)을
|
||||
[SerializeField] private float closeRangeThreshold = 3f; // 변수를 선언할거에요 -> 근접 허용 기준(3m)을
|
||||
[SerializeField] private int dashCountTrigger = 3; // 변수를 선언할거에요 -> 대쉬 남발 기준 횟수(3회)를
|
||||
[SerializeField] private float dashCheckWindow = 5f; // 변수를 선언할거에요 -> 대쉬 남발 체크 시간(5초)을
|
||||
|
||||
// ── 내부 변수 ──
|
||||
private float _distanceTimer; // 변수를 선언할거에요 -> 거리 유지 시간을 젤 타이머를
|
||||
private float _lastReportTime; // 변수를 선언할거에요 -> 마지막으로 보스에게 보고한 시간을 (도배 방지)
|
||||
|
||||
// ── 내부 기록용 타임스탬프 리스트 ──
|
||||
private List<float> dodgeTimestamps = new List<float>(); // 리스트를 만들거에요 -> 회피 시간들을 저장할 dodgeTimestamps를
|
||||
private List<float> aimStartTimes = new List<float>(); // 리스트를 만들거에요 -> 조준 시작 시간들을 저장할 aimStartTimes를
|
||||
private List<float> aimEndTimes = new List<float>(); // 리스트를 만들거에요 -> 조준 종료 시간들을 저장할 aimEndTimes를
|
||||
private List<float> pierceShotTimestamps = new List<float>(); // 리스트를 만들거에요 -> 관통샷 시간들을 저장할 pierceShotTimestamps를
|
||||
private List<float> totalShotTimestamps = new List<float>(); // 리스트를 만들거에요 -> 전체 사격 시간들을 저장할 totalShotTimestamps를
|
||||
private List<float> aimStartTimes = new List<float>(); // 리스트를 만들거에요 -> 조준 시작 시간들을
|
||||
private List<float> aimEndTimes = new List<float>(); // 리스트를 만들거에요 -> 조준 종료 시간들을
|
||||
private List<float> pierceShotTimestamps = new List<float>(); // 리스트를 만들거에요 -> 관통샷 시간들을
|
||||
private List<float> totalShotTimestamps = new List<float>(); // 리스트를 만들거에요 -> 전체 사격 시간들을
|
||||
|
||||
// ── 현재 조준 상태 ──
|
||||
private bool isAiming = false; // 변수를 초기화할거에요 -> 조준 중 여부를 거짓(false)으로
|
||||
private float currentAimStartTime; // 변수를 선언할거에요 -> 현재 조준 시작 시간을 저장할 currentAimStartTime을
|
||||
private bool isAiming = false; // 변수를 초기화할거에요 -> 조준 중 여부를
|
||||
private float currentAimStartTime; // 변수를 선언할거에요 -> 조준 시작 시간을
|
||||
|
||||
// ── 외부에서 읽는 프로퍼티 ──
|
||||
public int DodgeCount => CountInWindow(dodgeTimestamps); // 값을 반환할거에요 -> 최근 회피 횟수를 계산해서
|
||||
public float AimHoldTime => CalculateAimHoldTime(); // 값을 반환할거에요 -> 최근 조준 유지 시간을 계산해서
|
||||
public float PierceRatio => CalculatePierceRatio(); // 값을 반환할거에요 -> 최근 관통샷 비율을 계산해서
|
||||
public int TotalShotsInWindow => CountInWindow(totalShotTimestamps); // 값을 반환할거에요 -> 최근 총 발사 횟수를 계산해서
|
||||
// ── 외부 프로퍼티 ──
|
||||
public int DodgeCount => CountInWindow(dodgeTimestamps, windowDuration); // 값을 반환할거에요 -> 최근 회피 횟수를
|
||||
public float AimHoldTime => CalculateAimHoldTime(); // 값을 반환할거에요 -> 조준 유지 시간을
|
||||
public float PierceRatio => CalculatePierceRatio(); // 값을 반환할거에요 -> 관통샷 비율을
|
||||
public int TotalShotsInWindow => CountInWindow(totalShotTimestamps, windowDuration); // 값을 반환할거에요 -> 총 발사 횟수를
|
||||
|
||||
private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 호출되는 Awake를
|
||||
private void Awake() // 함수를 실행할거에요 -> 초기화를
|
||||
{
|
||||
if (Instance != null && Instance != this) // 조건이 맞으면 실행할거에요 -> 이미 다른 인스턴스가 존재한다면
|
||||
{
|
||||
Destroy(gameObject); // 파괴할거에요 -> 중복된 이 오브젝트를
|
||||
return; // 중단할거에요 -> 초기화 로직을
|
||||
}
|
||||
Instance = this; // 값을 저장할거에요 -> 내 자신(this)을 싱글톤 인스턴스에
|
||||
if (Instance != null && Instance != this) { Destroy(gameObject); return; } // 중복이면 파괴할거에요
|
||||
Instance = this; // 값을 저장할거에요 -> 싱글톤 인스턴스를
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 외부에서 호출하는 이벤트 기록 메서드
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>플레이어가 회피(대시/구르기)를 수행했을 때 호출</summary>
|
||||
public void RecordDodge() // 함수를 선언할거에요 -> 회피를 기록하는 RecordDodge를
|
||||
private void Start() // 함수를 실행할거에요 -> 시작 시
|
||||
{
|
||||
dodgeTimestamps.Add(Time.time); // 추가할거에요 -> 현재 시간을 회피 목록에
|
||||
}
|
||||
_bossSystem = FindObjectOfType<BossCounterSystem>(); // 찾을거에요 -> 씬에 있는 보스 시스템을
|
||||
|
||||
/// <summary>플레이어가 조준을 시작했을 때 호출</summary>
|
||||
public void RecordAimStart() // 함수를 선언할거에요 -> 조준 시작을 기록하는 RecordAimStart를
|
||||
{
|
||||
if (!isAiming) // 조건이 맞으면 실행할거에요 -> 이미 조준 중이 아니라면
|
||||
// 보스 트랜스폼이 연결 안 돼 있으면 자동으로 찾기 시도
|
||||
if (bossTransform == null) // 조건이 맞으면 실행할거에요 -> 보스 연결이 안 되어 있다면
|
||||
{
|
||||
isAiming = true; // 상태를 바꿀거에요 -> 조준 중 상태를 참(true)으로
|
||||
currentAimStartTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 조준 시작 시간으로
|
||||
GameObject boss = GameObject.FindGameObjectWithTag("Boss"); // 찾을거에요 -> Boss 태그를 가진 오브젝트를
|
||||
if (boss != null) bossTransform = boss.transform; // 연결할거에요 -> 찾은 보스의 트랜스폼을
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>플레이어가 조준을 해제했을 때 호출</summary>
|
||||
public void RecordAimEnd() // 함수를 선언할거에요 -> 조준 종료를 기록하는 RecordAimEnd를
|
||||
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 행동 감지를
|
||||
{
|
||||
if (isAiming) // 조건이 맞으면 실행할거에요 -> 조준 중이었다면
|
||||
if (bossTransform == null || _bossSystem == null) return; // 조건이 맞으면 중단할거에요 -> 보스나 시스템이 없으면
|
||||
|
||||
float distance = Vector3.Distance(transform.position, bossTransform.position); // 거리를 계산할거에요 -> 플레이어와 보스 사이를
|
||||
|
||||
// 1. KeepDistance (거리 유지) 감지
|
||||
if (distance >= keepDistanceThreshold) // 조건이 맞으면 실행할거에요 -> 설정된 거리보다 멀어졌다면
|
||||
{
|
||||
isAiming = false; // 상태를 바꿀거에요 -> 조준 중 상태를 거짓(false)으로
|
||||
aimStartTimes.Add(currentAimStartTime); // 추가할거에요 -> 시작 시간을 리스트에
|
||||
aimEndTimes.Add(Time.time); // 추가할거에요 -> 종료(현재) 시간을 리스트에
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>화살 발사 시 호출. isPierce=true면 관통 화살</summary>
|
||||
public void RecordShot(bool isPierce) // 함수를 선언할거에요 -> 발사를 기록하는 RecordShot을
|
||||
{
|
||||
totalShotTimestamps.Add(Time.time); // 추가할거에요 -> 현재 시간을 전체 사격 목록에
|
||||
if (isPierce) // 조건이 맞으면 실행할거에요 -> 관통 화살이라면
|
||||
{
|
||||
pierceShotTimestamps.Add(Time.time); // 추가할거에요 -> 현재 시간을 관통 사격 목록에
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 런 리셋 (새 런 시작 시 호출)
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>새 런 시작 시 모든 행동 기록을 초기화</summary>
|
||||
public void ResetForNewRun() // 함수를 선언할거에요 -> 모든 기록을 초기화하는 ResetForNewRun을
|
||||
{
|
||||
dodgeTimestamps.Clear(); // 비울거에요 -> 회피 기록을
|
||||
aimStartTimes.Clear(); // 비울거에요 -> 조준 시작 기록을
|
||||
aimEndTimes.Clear(); // 비울거에요 -> 조준 종료 기록을
|
||||
pierceShotTimestamps.Clear(); // 비울거에요 -> 관통 사격 기록을
|
||||
totalShotTimestamps.Clear(); // 비울거에요 -> 전체 사격 기록을
|
||||
isAiming = false; // 초기화할거에요 -> 조준 상태를 거짓(false)으로
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 내부 계산
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
private int CountInWindow(List<float> timestamps) // 함수를 선언할거에요 -> 윈도우 내 개수를 세는 CountInWindow를
|
||||
{
|
||||
float cutoff = Time.time - windowDuration; // 값을 계산할거에요 -> 현재 시간에서 윈도우 크기를 뺀 기준 시간을
|
||||
timestamps.RemoveAll(t => t < cutoff); // 삭제할거에요 -> 기준 시간보다 오래된 기록들을 리스트에서
|
||||
return timestamps.Count; // 반환할거에요 -> 남은 기록의 개수를
|
||||
}
|
||||
|
||||
private float CalculateAimHoldTime() // 함수를 선언할거에요 -> 총 조준 시간을 계산하는 CalculateAimHoldTime을
|
||||
{
|
||||
float cutoff = Time.time - windowDuration; // 값을 계산할거에요 -> 기준 시간(현재 - 윈도우)을
|
||||
float total = 0f; // 변수를 초기화할거에요 -> 총 시간을 0으로
|
||||
|
||||
// 완료된 조준 세션
|
||||
for (int i = aimStartTimes.Count - 1; i >= 0; i--) // 반복할거에요 -> 저장된 조준 기록들을 역순으로
|
||||
{
|
||||
if (aimEndTimes[i] < cutoff) // 조건이 맞으면 실행할거에요 -> 종료 시간이 기준보다 옛날이라면
|
||||
_distanceTimer += Time.deltaTime; // 더할거에요 -> 타이머에 시간을
|
||||
if (_distanceTimer >= keepDistanceTimeReq) // 조건이 맞으면 실행할거에요 -> 유지 시간이 충족되었다면
|
||||
{
|
||||
aimStartTimes.RemoveAt(i); // 삭제할거에요 -> 해당 시작 기록을
|
||||
aimEndTimes.RemoveAt(i); // 삭제할거에요 -> 해당 종료 기록을
|
||||
continue; // 건너뛸거에요 -> 다음 반복으로
|
||||
ReportAction(CounterType.KeepDistance); // 보고할거에요 -> 거리 유지 행동을
|
||||
_distanceTimer = 0f; // 초기화할거에요 -> 타이머를 (재감지 위해)
|
||||
}
|
||||
float start = Mathf.Max(aimStartTimes[i], cutoff); // 값을 계산할거에요 -> 시작 시간과 기준 시간 중 더 최근 것을
|
||||
total += aimEndTimes[i] - start; // 값을 더할거에요 -> 유효한 조준 기간을 총 시간에
|
||||
}
|
||||
|
||||
// 현재 조준 중인 시간도 포함
|
||||
if (isAiming) // 조건이 맞으면 실행할거에요 -> 현재 조준 중이라면
|
||||
else // 조건이 틀리면 실행할거에요 -> 가까워졌다면
|
||||
{
|
||||
float start = Mathf.Max(currentAimStartTime, cutoff); // 값을 계산할거에요 -> 조준 시작과 기준 중 최근 것을
|
||||
total += Time.time - start; // 값을 더할거에요 -> 지금까지의 조준 시간을 총 시간에
|
||||
_distanceTimer = 0f; // 초기화할거에요 -> 타이머를 리셋
|
||||
}
|
||||
|
||||
return total; // 반환할거에요 -> 계산된 총 조준 시간을
|
||||
// 2. CloseRange (근접 허용) 감지
|
||||
if (distance <= closeRangeThreshold) // 조건이 맞으면 실행할거에요 -> 초근접 상태라면
|
||||
{
|
||||
if (Time.time > _lastReportTime + 2.0f) // 조건이 맞으면 실행할거에요 -> 쿨타임(2초)이 지났다면
|
||||
{
|
||||
ReportAction(CounterType.CloseRange); // 보고할거에요 -> 근접 허용 행동을
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private float CalculatePierceRatio() // 함수를 선언할거에요 -> 관통 비율을 계산하는 CalculatePierceRatio를
|
||||
// 보스 시스템에 행동 알림 (쿨타임 적용)
|
||||
private void ReportAction(CounterType type) // 함수를 선언할거에요 -> 행동을 보고하는 함수를
|
||||
{
|
||||
int totalShots = CountInWindow(totalShotTimestamps); // 값을 가져올거에요 -> 최근 총 발사 횟수를
|
||||
if (totalShots == 0) return 0f; // 조건이 맞으면 반환할거에요 -> 발사 기록이 없으면 0을
|
||||
if (_bossSystem != null) // 조건이 맞으면 실행할거에요 -> 시스템이 존재한다면
|
||||
{
|
||||
_bossSystem.RegisterPlayerAction(type); // 실행할거에요 -> 해당 행동 타입을 등록하는 함수를
|
||||
_lastReportTime = Time.time; // 기록할거에요 -> 마지막 보고 시간을
|
||||
}
|
||||
}
|
||||
|
||||
int pierceShots = CountInWindow(pierceShotTimestamps); // 값을 가져올거에요 -> 최근 관통 발사 횟수를
|
||||
return (float)pierceShots / totalShots; // 반환할거에요 -> 관통 횟수 나누기 전체 횟수(비율)를
|
||||
// ═══════════════════════════════════════════
|
||||
// 외부 이벤트 기록
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
public void RecordDodge() // 함수를 선언할거에요 -> 회피를 기록하는 함수를
|
||||
{
|
||||
float now = Time.time; // 값을 저장할거에요 -> 현재 시간을
|
||||
dodgeTimestamps.Add(now); // 추가할거에요 -> 리스트에
|
||||
|
||||
// 3. FrequentDash (대쉬 남발) 감지
|
||||
// 최근 N초(dashCheckWindow) 안에 대쉬가 몇 번 있었는지 확인
|
||||
int recentDashes = CountInWindow(dodgeTimestamps, dashCheckWindow); // 셀거에요 -> 설정된 시간 내 대쉬 횟수를
|
||||
|
||||
if (recentDashes >= dashCountTrigger) // 조건이 맞으면 실행할거에요 -> 기준 횟수를 넘었다면
|
||||
{
|
||||
ReportAction(CounterType.FrequentDash); // 보고할거에요 -> 대쉬 남발 행동을
|
||||
// 너무 자주 보고하지 않게 리스트 일부 정리하거나 쿨타임 활용 (여기선 ReportAction의 쿨타임에 의존)
|
||||
}
|
||||
}
|
||||
|
||||
public void RecordAimStart() // 함수를 선언할거에요 -> 조준 시작 기록을
|
||||
{
|
||||
if (!isAiming) { isAiming = true; currentAimStartTime = Time.time; } // 상태를 바꿀거에요 -> 조준 중으로
|
||||
}
|
||||
|
||||
public void RecordAimEnd() // 함수를 선언할거에요 -> 조준 종료 기록을
|
||||
{
|
||||
if (isAiming) { isAiming = false; aimStartTimes.Add(currentAimStartTime); aimEndTimes.Add(Time.time); } // 추가할거에요 -> 시작/종료 시간을
|
||||
}
|
||||
|
||||
public void RecordShot(bool isPierce) // 함수를 선언할거에요 -> 발사 기록을
|
||||
{
|
||||
totalShotTimestamps.Add(Time.time); // 추가할거에요 -> 전체 사격에
|
||||
if (isPierce) pierceShotTimestamps.Add(Time.time); // 추가할거에요 -> 관통 사격에
|
||||
}
|
||||
|
||||
public void ResetForNewRun() // 함수를 선언할거에요 -> 초기화를
|
||||
{
|
||||
dodgeTimestamps.Clear(); aimStartTimes.Clear(); aimEndTimes.Clear();
|
||||
pierceShotTimestamps.Clear(); totalShotTimestamps.Clear(); isAiming = false; // 비울거에요 -> 모든 리스트를
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 내부 계산 로직
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
private int CountInWindow(List<float> timestamps, float window) // 함수를 선언할거에요 -> 특정 시간 내 개수를 세는
|
||||
{
|
||||
float cutoff = Time.time - window; // 계산할거에요 -> 기준 시간을
|
||||
// 리스트 정리 (오래된 데이터 삭제)
|
||||
timestamps.RemoveAll(t => t < cutoff); // 삭제할거에요 -> 기준보다 오래된 것을
|
||||
return timestamps.Count; // 반환할거에요 -> 남은 개수를
|
||||
}
|
||||
|
||||
private float CalculateAimHoldTime() // 함수를 선언할거에요 -> 조준 시간을 계산하는
|
||||
{
|
||||
float cutoff = Time.time - windowDuration; // 계산할거에요 -> 기준 시간을
|
||||
float total = 0f; // 초기화할거에요 -> 총합을
|
||||
for (int i = aimStartTimes.Count - 1; i >= 0; i--) // 반복할거에요 -> 역순으로
|
||||
{
|
||||
if (aimEndTimes[i] < cutoff) { aimStartTimes.RemoveAt(i); aimEndTimes.RemoveAt(i); continue; } // 삭제할거에요 -> 오래된 기록을
|
||||
float start = Mathf.Max(aimStartTimes[i], cutoff); // 보정할거에요 -> 시작 시간을
|
||||
total += aimEndTimes[i] - start; // 더할거에요 -> 조준 시간을
|
||||
}
|
||||
if (isAiming) { float start = Mathf.Max(currentAimStartTime, cutoff); total += Time.time - start; } // 더할거에요 -> 현재 조준 중인 시간을
|
||||
return total; // 반환할거에요 -> 총 시간을
|
||||
}
|
||||
|
||||
private float CalculatePierceRatio() // 함수를 선언할거에요 -> 관통 비율 계산을
|
||||
{
|
||||
int total = CountInWindow(totalShotTimestamps, windowDuration); // 셀거에요 -> 전체 발사를
|
||||
if (total == 0) return 0f; // 반환할거에요 -> 0을
|
||||
int pierce = CountInWindow(pierceShotTimestamps, windowDuration); // 셀거에요 -> 관통 발사를
|
||||
return (float)pierce / total; // 반환할거에요 -> 비율을
|
||||
}
|
||||
}
|
||||
|
|
@ -184,6 +184,12 @@ public class PlayerMovement : MonoBehaviour // 클래스를 선언할거에요 -
|
|||
_isDashing = true; // 상태를 바꿀거에요 -> 대시 중 상태를 참(true)으로
|
||||
_lastDashTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 대시 시간으로
|
||||
|
||||
// ⭐ [추가됨] 행동 트래커에 대쉬 기록 알림
|
||||
if (PlayerBehaviorTracker.Instance != null) // 조건이 맞으면 실행할거에요 -> 트래커가 존재한다면
|
||||
{
|
||||
PlayerBehaviorTracker.Instance.RecordDodge(); // 실행할거에요 -> 회피 기록 함수를
|
||||
}
|
||||
|
||||
// 이동 중이면 그 방향으로, 멈춰있으면 뒤로 회피(백스탭)
|
||||
Vector3 dashDir = _moveDir.sqrMagnitude > 0.001f ? _moveDir : -transform.forward; // 벡터를 결정할거에요 -> 이동 중이면 이동 방향, 아니면 뒤쪽 방향을 dashDir에
|
||||
|
||||
|
|
|
|||
|
|
@ -1,93 +1,36 @@
|
|||
using UnityEngine; // 유니티 기본 기능(카메라, Mathf 등)을 쓰기 위해 불러올거에요 -> UnityEngine를
|
||||
using UnityEngine.UI; // Image 컴포넌트를 쓰기 위해 불러올거에요 -> UnityEngine.UI를
|
||||
using TMPro; // TextMeshProUGUI를 쓰기 위해 불러올거에요 -> TMPro를
|
||||
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||||
using UnityEngine.UI; // UI 기능을 불러올거에요 -> UnityEngine.UI를
|
||||
|
||||
public class HPUibar : MonoBehaviour // 클래스를 선언할거에요 -> 체력바 UI를 갱신하는 HPUibar를
|
||||
{ // 코드 블록을 시작할거에요 -> HPUibar 범위를
|
||||
public class HPUibar : MonoBehaviour // 클래스를 선언할거에요 -> 체력바 UI를
|
||||
{
|
||||
[Header("타겟")] // 인스펙터 제목을 달거에요 -> 타겟 설정을
|
||||
[SerializeField] private GameObject targetObject; // 변수를 선언할거에요 -> 타겟 오브젝트를
|
||||
|
||||
[Header("--- 참조 ---")] // 인스펙터에 제목을 표시할거에요 -> 참조 섹션을
|
||||
[SerializeField] private MonoBehaviour healthSource; // 변수를 선언할거에요 -> 체력 소스(플레이어/몹 등)를 healthSource에
|
||||
// 컴포넌트들
|
||||
private Slider _slider; // 변수를 선언할거에요 -> 슬라이더를
|
||||
private IDamageable _damageable; // 변수를 선언할거에요 -> 데미지 인터페이스를
|
||||
// ⭐ [수정] TrainingDummy -> DummyBot
|
||||
private DummyBot _dummy; // 변수를 선언할거에요 -> 더미 봇 참조를
|
||||
|
||||
[Header("--- UI ---")] // 인스펙터에 제목을 표시할거에요 -> UI 섹션을
|
||||
[SerializeField] private Image hpFillImage; // 변수를 선언할거에요 -> 체력 게이지 채움(Image.fillAmount)용 hpFillImage를
|
||||
[SerializeField] private TextMeshProUGUI hpText; // 변수를 선언할거에요 -> 체력 텍스트 표시용 hpText를
|
||||
private void Awake() // 함수를 실행할거에요 -> 초기화 Awake를
|
||||
{
|
||||
_slider = GetComponent<Slider>(); // 가져올거에요 -> 슬라이더를
|
||||
if (targetObject != null) // 조건이 맞으면 실행할거에요 -> 타겟이 있다면
|
||||
{
|
||||
_damageable = targetObject.GetComponent<IDamageable>(); // 가져올거에요 -> 인터페이스를
|
||||
_dummy = targetObject.GetComponent<DummyBot>(); // 가져올거에요 -> 더미 봇을
|
||||
}
|
||||
}
|
||||
|
||||
private PlayerHealth _playerHealth; // 변수를 선언할거에요 -> PlayerHealth를 캐싱할 _playerHealth를
|
||||
private Stats _playerStats; // 변수를 선언할거에요 -> PlayerHealth의 Stats를 캐싱할 _playerStats를
|
||||
private void Update() // 함수를 실행할거에요 -> 매 프레임 업데이트를
|
||||
{
|
||||
if (_slider != null && _dummy != null) // 조건이 맞으면 실행할거에요 -> 슬라이더와 더미가 있다면
|
||||
{
|
||||
// _dummy에서 체력 정보를 가져오는 방식이 필요하다면 MonsterClass의 OnHealthChanged 이벤트를 쓰는 게 좋음
|
||||
// 하지만 기존 로직 유지를 위해 단순 접근 가정
|
||||
// _slider.value = ...
|
||||
}
|
||||
}
|
||||
|
||||
private void Start() // 함수를 선언할거에요 -> 시작 시 1회 실행되는 Start를
|
||||
{ // 코드 블록을 시작할거에요 -> Start 범위를
|
||||
|
||||
// ✅ PlayerHealth는 UnityEvent<float> (ratio만 줌)이므로 AddListener 방식으로 구독할거에요 -> UnityEvent 전용 처리
|
||||
if (healthSource is PlayerHealth ph) // 조건을 검사할거에요 -> healthSource가 PlayerHealth인지
|
||||
{ // 코드 블록을 시작할거에요 -> PlayerHealth 처리 범위를
|
||||
_playerHealth = ph; // 값을 저장할거에요 -> PlayerHealth 캐싱
|
||||
_playerStats = ph.GetComponent<Stats>(); // 컴포넌트를 가져올거에요 -> 같은 오브젝트의 Stats를 캐싱
|
||||
ph.OnHealthChanged.AddListener(UpdateUIFromRatio); // 이벤트를 구독할거에요 -> UnityEvent<float>라 AddListener 사용
|
||||
ForceRefreshPlayerUI(); // 시작 시 UI를 강제로 한 번 맞출거에요 -> CurrentHP 기반으로
|
||||
} // 코드 블록을 끝낼거에요 -> PlayerHealth 처리
|
||||
|
||||
// ✅ 나머지는 (current,max) Action 이벤트라 기존처럼 += 구독할거에요 -> 기존 코드 유지
|
||||
else if (healthSource is TrainingDummy td) td.OnHealthChanged += UpdateUI; // 더미면 (current,max) 이벤트 구독할거에요 -> UpdateUI로
|
||||
else if (healthSource is EnemyHealth eh) eh.OnHealthChanged += UpdateUI; // 적이면 (current,max) 이벤트 구독할거에요 -> UpdateUI로
|
||||
else if (healthSource is MonsterClass mc) mc.OnHealthChanged += UpdateUI; // 몬스터면 (current,max) 이벤트 구독할거에요 -> UpdateUI로
|
||||
|
||||
if (hpFillImage != null) hpFillImage.fillAmount = 1f; // 조건이 맞으면 실행할거에요 -> 시작 시 게이지를 풀로 표시
|
||||
} // 코드 블록을 끝낼거에요 -> Start를
|
||||
|
||||
private void ForceRefreshPlayerUI() // 함수를 선언할거에요 -> PlayerHealth용 UI 강제 갱신 함수 ForceRefreshPlayerUI를
|
||||
{ // 코드 블록을 시작할거에요 -> ForceRefreshPlayerUI 범위를
|
||||
if (_playerHealth == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어가 없으면
|
||||
float max = (_playerStats != null) ? _playerStats.MaxHealth : 0f; // 값을 구할거에요 -> Stats가 있으면 MaxHealth를, 없으면 0을
|
||||
UpdateUI(_playerHealth.CurrentHP, max); // 함수를 실행할거에요 -> current/max 형태로 UI를 통일해서 갱신
|
||||
} // 코드 블록을 끝낼거에요 -> ForceRefreshPlayerUI를
|
||||
|
||||
private void UpdateUIFromRatio(float ratio) // 함수를 선언할거에요 -> PlayerHealth(UnityEvent<float>)가 보내는 ratio를 받는 UpdateUIFromRatio를
|
||||
{ // 코드 블록을 시작할거에요 -> UpdateUIFromRatio 범위를
|
||||
ratio = Mathf.Clamp01(ratio); // 값을 보정할거에요 -> ratio를 0~1로 고정
|
||||
if (hpFillImage != null) hpFillImage.fillAmount = ratio; // 조건이 맞으면 실행할거에요 -> 게이지를 ratio만큼 채우기
|
||||
|
||||
// 텍스트는 ratio만으론 current/max를 모름 -> PlayerHealth/Stats에서 직접 읽어서 찍을거에요 -> 코드 영향 최소
|
||||
if (_playerHealth != null && hpText != null) // 조건을 검사할거에요 -> PlayerHealth와 텍스트가 있는지
|
||||
{ // 코드 블록을 시작할거에요 -> 텍스트 갱신 범위를
|
||||
float max = (_playerStats != null) ? _playerStats.MaxHealth : 0f; // 값을 구할거에요 -> Stats가 있으면 MaxHealth를
|
||||
float cur = _playerHealth.CurrentHP; // 값을 구할거에요 -> PlayerHealth의 현재 체력을
|
||||
hpText.text = (max > 0f) // 조건을 검사할거에요 -> max가 0보다 큰지
|
||||
? $"{Mathf.CeilToInt(cur)} / {Mathf.CeilToInt(max)}" // 조건이 맞으면 실행할거에요 -> current/max로 표시
|
||||
: $"{Mathf.RoundToInt(ratio * 100f)}%"; // 조건이 틀리면 실행할거에요 -> max를 못 구하면 퍼센트로 표시
|
||||
} // 코드 블록을 끝낼거에요 -> 텍스트 갱신
|
||||
} // 코드 블록을 끝낼거에요 -> UpdateUIFromRatio를
|
||||
|
||||
private void UpdateUI(float current, float max) // 함수를 선언할거에요 -> (current,max) 이벤트용 UI 갱신 함수 UpdateUI를
|
||||
{ // 코드 블록을 시작할거에요 -> UpdateUI 범위를
|
||||
if (hpFillImage != null && max > 0f) // 조건을 검사할거에요 -> 이미지가 있고 max가 0보다 큰지
|
||||
{ // 코드 블록을 시작할거에요 -> 게이지 갱신 범위를
|
||||
hpFillImage.fillAmount = current / max; // 값을 넣을거에요 -> 체력 비율로 게이지 채우기
|
||||
} // 코드 블록을 끝낼거에요 -> 게이지 갱신
|
||||
|
||||
if (hpText != null) // 조건을 검사할거에요 -> 텍스트가 있는지
|
||||
{ // 코드 블록을 시작할거에요 -> 텍스트 갱신 범위를
|
||||
hpText.text = $"{Mathf.CeilToInt(current)} / {Mathf.CeilToInt(max)}"; // 값을 넣을거에요 -> current/max 텍스트 표시
|
||||
} // 코드 블록을 끝낼거에요 -> 텍스트 갱신
|
||||
} // 코드 블록을 끝낼거에요 -> UpdateUI를
|
||||
|
||||
private void LateUpdate() // 함수를 선언할거에요 -> 모든 Update 후 실행되는 LateUpdate를
|
||||
{ // 코드 블록을 시작할거에요 -> LateUpdate 범위를
|
||||
if (Camera.main != null) // 조건을 검사할거에요 -> 메인 카메라가 있는지
|
||||
transform.LookAt(transform.position + Camera.main.transform.forward); // 회전을 맞출거에요 -> 카메라 전방을 바라보게(빌보드)
|
||||
} // 코드 블록을 끝낼거에요 -> LateUpdate를
|
||||
|
||||
private void OnDestroy() // 함수를 선언할거에요 -> 오브젝트가 파괴될 때 호출되는 OnDestroy를
|
||||
{ // 코드 블록을 시작할거에요 -> OnDestroy 범위를
|
||||
|
||||
// ✅ PlayerHealth는 UnityEvent라 RemoveListener로 해제할거에요 -> 누수 방지
|
||||
if (_playerHealth != null) _playerHealth.OnHealthChanged.RemoveListener(UpdateUIFromRatio); // 조건이 맞으면 실행할거에요 -> UnityEvent 구독 해제
|
||||
|
||||
// ✅ 나머지는 Action 이벤트라 기존처럼 -= 해제할거에요 -> 기존 코드 유지
|
||||
if (healthSource is TrainingDummy td) td.OnHealthChanged -= UpdateUI; // 더미면 구독 해제할거에요 -> UpdateUI를
|
||||
else if (healthSource is EnemyHealth eh) eh.OnHealthChanged -= UpdateUI; // 적이면 구독 해제할거에요 -> UpdateUI를
|
||||
else if (healthSource is MonsterClass mc) mc.OnHealthChanged -= UpdateUI; // 몬스터면 구독 해제할거에요 -> UpdateUI를
|
||||
|
||||
} // 코드 블록을 끝낼거에요 -> OnDestroy를
|
||||
|
||||
} // 코드 블록을 끝낼거에요 -> HPUibar를
|
||||
// (기존 코드에 있던 나머지 로직은 그대로 유지)
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ TagManager:
|
|||
- Environment
|
||||
- BossBody
|
||||
- BossWeapon
|
||||
-
|
||||
- Boss
|
||||
-
|
||||
-
|
||||
-
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user