모스 몬스터
This commit is contained in:
parent
e126460285
commit
2a4c20435f
BIN
.plastic/plastic.changes
Normal file
BIN
.plastic/plastic.changes
Normal file
Binary file not shown.
Binary file not shown.
|
|
@ -4383,6 +4383,74 @@ Transform:
|
||||||
m_CorrespondingSourceObject: {fileID: 4708095961488610, guid: 01d14f7c3a49b4f4e86caee63ef815b1, type: 3}
|
m_CorrespondingSourceObject: {fileID: 4708095961488610, guid: 01d14f7c3a49b4f4e86caee63ef815b1, type: 3}
|
||||||
m_PrefabInstance: {fileID: 47955374}
|
m_PrefabInstance: {fileID: 47955374}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
--- !u!1001 &48215575
|
||||||
|
PrefabInstance:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Modification:
|
||||||
|
serializedVersion: 3
|
||||||
|
m_TransformParent: {fileID: 0}
|
||||||
|
m_Modifications:
|
||||||
|
- target: {fileID: 186042788264501781, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3}
|
||||||
|
propertyPath: m_WarningMessage
|
||||||
|
value: "\nBinding warning: Some generic clip(s) animate transforms that are
|
||||||
|
already bound by a Humanoid avatar. These transforms can only be changed
|
||||||
|
by Humanoid clips.\n\tTransform 'chin'\n\tTransform 'pelvis'\n\tTransform
|
||||||
|
'index_03_l'\n\tTransform 'lowerarm_l'\n\tTransform 'chin'\n\tTransform 'neck_01'\n\tTransform
|
||||||
|
'clavicle_l'\n\tTransform 'ring_02_l'\n\tTransform 'ball_l'\n\tTransform
|
||||||
|
'hand_l'\n\tand more ...\n\tFrom animation clip 'idle'\n\tFrom animation
|
||||||
|
clip 'run'\n\tFrom animation clip 'ghoul_walk'\n\tFrom animation clip 'ghoul_die'\n\tFrom
|
||||||
|
animation clip 'ghoul_gethit'"
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 5674935864780053661, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3}
|
||||||
|
propertyPath: m_Name
|
||||||
|
value: SwordMonster
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3}
|
||||||
|
propertyPath: m_LocalPosition.x
|
||||||
|
value: 19.30664
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3}
|
||||||
|
propertyPath: m_LocalPosition.y
|
||||||
|
value: 7.250199
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3}
|
||||||
|
propertyPath: m_LocalPosition.z
|
||||||
|
value: 14.687386
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3}
|
||||||
|
propertyPath: m_LocalRotation.w
|
||||||
|
value: 1
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3}
|
||||||
|
propertyPath: m_LocalRotation.x
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3}
|
||||||
|
propertyPath: m_LocalRotation.y
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3}
|
||||||
|
propertyPath: m_LocalRotation.z
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3}
|
||||||
|
propertyPath: m_LocalEulerAnglesHint.x
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3}
|
||||||
|
propertyPath: m_LocalEulerAnglesHint.y
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3}
|
||||||
|
propertyPath: m_LocalEulerAnglesHint.z
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
m_RemovedComponents: []
|
||||||
|
m_RemovedGameObjects: []
|
||||||
|
m_AddedGameObjects: []
|
||||||
|
m_AddedComponents: []
|
||||||
|
m_SourcePrefab: {fileID: 100100000, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3}
|
||||||
--- !u!1001 &48315642
|
--- !u!1001 &48315642
|
||||||
PrefabInstance:
|
PrefabInstance:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -9052,14 +9120,14 @@ MonoBehaviour:
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 85056388}
|
m_GameObject: {fileID: 85056388}
|
||||||
m_Enabled: 1
|
m_Enabled: 0
|
||||||
m_EditorHideFlags: 0
|
m_EditorHideFlags: 0
|
||||||
m_Script: {fileID: 11500000, guid: 1f525fb5022b0754b9c5e1d725f8b2a4, type: 3}
|
m_Script: {fileID: 11500000, guid: 1f525fb5022b0754b9c5e1d725f8b2a4, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
spawnRange: 15
|
spawnRange: 15
|
||||||
respawnCooldown: 3
|
respawnCooldown: 3
|
||||||
mobTag: Boss
|
mobTag: Throw Monster
|
||||||
optimizationRange: 60
|
optimizationRange: 60
|
||||||
--- !u!4 &85056390
|
--- !u!4 &85056390
|
||||||
Transform:
|
Transform:
|
||||||
|
|
@ -39085,9 +39153,21 @@ MonoBehaviour:
|
||||||
impactSpawnPoint: {fileID: 0}
|
impactSpawnPoint: {fileID: 0}
|
||||||
counterSystem: {fileID: 358757530}
|
counterSystem: {fileID: 358757530}
|
||||||
patternInterval: 3
|
patternInterval: 3
|
||||||
ironBall: {fileID: 0}
|
attackRange: 3
|
||||||
handHolder: {fileID: 0}
|
ironBall: {fileID: 1550577165}
|
||||||
|
handHolder: {fileID: 1325714516}
|
||||||
bossHealthBar: {fileID: 0}
|
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
|
--- !u!136 &358757532
|
||||||
CapsuleCollider:
|
CapsuleCollider:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -39158,7 +39238,7 @@ Rigidbody:
|
||||||
m_UseGravity: 1
|
m_UseGravity: 1
|
||||||
m_IsKinematic: 1
|
m_IsKinematic: 1
|
||||||
m_Interpolate: 0
|
m_Interpolate: 0
|
||||||
m_Constraints: 0
|
m_Constraints: 80
|
||||||
m_CollisionDetection: 0
|
m_CollisionDetection: 0
|
||||||
--- !u!1001 &358842824
|
--- !u!1001 &358842824
|
||||||
PrefabInstance:
|
PrefabInstance:
|
||||||
|
|
@ -66018,7 +66098,7 @@ GameObject:
|
||||||
m_Icon: {fileID: 0}
|
m_Icon: {fileID: 0}
|
||||||
m_NavMeshLayer: 0
|
m_NavMeshLayer: 0
|
||||||
m_StaticEditorFlags: 0
|
m_StaticEditorFlags: 0
|
||||||
m_IsActive: 1
|
m_IsActive: 0
|
||||||
--- !u!114 &600231663
|
--- !u!114 &600231663
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -66451,8 +66531,8 @@ MonoBehaviour:
|
||||||
m_Script: {fileID: 11500000, guid: 9eb5b341f7503494e9b0f88cc4b1c17d, type: 3}
|
m_Script: {fileID: 11500000, guid: 9eb5b341f7503494e9b0f88cc4b1c17d, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
healthSource: {fileID: 2112919314}
|
healthSource: {fileID: 0}
|
||||||
hpFillImage: {fileID: 910004995}
|
hpFillImage: {fileID: 0}
|
||||||
hpText: {fileID: 0}
|
hpText: {fileID: 0}
|
||||||
--- !u!1001 &603601734
|
--- !u!1001 &603601734
|
||||||
PrefabInstance:
|
PrefabInstance:
|
||||||
|
|
@ -68541,6 +68621,11 @@ Transform:
|
||||||
m_CorrespondingSourceObject: {fileID: 4012526298001850, guid: 1b1f948f132c1cc429df46b11756f4a2, type: 3}
|
m_CorrespondingSourceObject: {fileID: 4012526298001850, guid: 1b1f948f132c1cc429df46b11756f4a2, type: 3}
|
||||||
m_PrefabInstance: {fileID: 619812415}
|
m_PrefabInstance: {fileID: 619812415}
|
||||||
m_PrefabAsset: {fileID: 0}
|
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
|
--- !u!1 &620650552
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -115622,6 +115707,74 @@ Transform:
|
||||||
m_CorrespondingSourceObject: {fileID: 4872071498859388, guid: 5a0d3c35fdb1a0549ab15f2d38859ed1, type: 3}
|
m_CorrespondingSourceObject: {fileID: 4872071498859388, guid: 5a0d3c35fdb1a0549ab15f2d38859ed1, type: 3}
|
||||||
m_PrefabInstance: {fileID: 1080970801}
|
m_PrefabInstance: {fileID: 1080970801}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
--- !u!1 &1081393076
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1081393079}
|
||||||
|
- component: {fileID: 1081393078}
|
||||||
|
- component: {fileID: 1081393077}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: BossTriggerZone
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!114 &1081393077
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1081393076}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 086f3fc0675122448a8950f910dc4864, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier:
|
||||||
|
boss: {fileID: 358757531}
|
||||||
|
fogWall: {fileID: 0}
|
||||||
|
--- !u!65 &1081393078
|
||||||
|
BoxCollider:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1081393076}
|
||||||
|
m_Material: {fileID: 0}
|
||||||
|
m_IncludeLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
m_ExcludeLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
m_LayerOverridePriority: 0
|
||||||
|
m_IsTrigger: 1
|
||||||
|
m_ProvidesContacts: 0
|
||||||
|
m_Enabled: 1
|
||||||
|
serializedVersion: 3
|
||||||
|
m_Size: {x: 1, y: 1, z: 1}
|
||||||
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!4 &1081393079
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1081393076}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 26.738499, y: 13.2, z: 32.890816}
|
||||||
|
m_LocalScale: {x: 17.087, y: 17.087, z: 17.087}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1001 &1083084820
|
--- !u!1001 &1083084820
|
||||||
PrefabInstance:
|
PrefabInstance:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -118888,17 +119041,29 @@ PrefabInstance:
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_TransformParent: {fileID: 0}
|
m_TransformParent: {fileID: 0}
|
||||||
m_Modifications:
|
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}
|
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||||
propertyPath: m_LocalPosition.x
|
propertyPath: m_LocalPosition.x
|
||||||
value: 28.143673
|
value: 27.059
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||||
propertyPath: m_LocalPosition.y
|
propertyPath: m_LocalPosition.y
|
||||||
value: 5.500213
|
value: 5.48
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||||
propertyPath: m_LocalPosition.z
|
propertyPath: m_LocalPosition.z
|
||||||
value: 32.754795
|
value: 33.461
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||||
propertyPath: m_LocalRotation.w
|
propertyPath: m_LocalRotation.w
|
||||||
|
|
@ -118932,13 +119097,24 @@ PrefabInstance:
|
||||||
propertyPath: m_Name
|
propertyPath: m_Name
|
||||||
value: BossMonster 3D model
|
value: BossMonster 3D model
|
||||||
objectReference: {fileID: 0}
|
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}
|
- target: {fileID: 5866666021909216657, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||||
propertyPath: m_Controller
|
propertyPath: m_Controller
|
||||||
value:
|
value:
|
||||||
objectReference: {fileID: 9100000, guid: b207531287111e74d890e730f7278dfa, type: 2}
|
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_RemovedComponents: []
|
||||||
m_RemovedGameObjects: []
|
m_RemovedGameObjects: []
|
||||||
m_AddedGameObjects: []
|
m_AddedGameObjects:
|
||||||
|
- targetCorrespondingSourceObject: {fileID: 1852576806548013000, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||||
|
insertIndex: -1
|
||||||
|
addedObject: {fileID: 1325714516}
|
||||||
m_AddedComponents:
|
m_AddedComponents:
|
||||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
|
||||||
insertIndex: -1
|
insertIndex: -1
|
||||||
|
|
@ -142858,13 +143034,13 @@ Transform:
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 1325714515}
|
m_GameObject: {fileID: 1325714515}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: -0.0026552675, y: 0.03898671, z: 0.72380346, w: -0.6888988}
|
m_LocalRotation: {x: -0.038344428, y: 0.70765644, z: -0.0024401005, w: -0.7055112}
|
||||||
m_LocalPosition: {x: 30.757803, y: 8.46146, z: 31.390688}
|
m_LocalPosition: {x: 1.304071, y: 1.8877394, z: -1.5619158}
|
||||||
m_LocalScale: {x: 234.34998, y: 234.35007, z: 234.35002}
|
m_LocalScale: {x: 234.34999, y: 234.35004, z: 234.35002}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children:
|
m_Children:
|
||||||
- {fileID: 1550577166}
|
- {fileID: 1550577166}
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 620355262}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1001 &1326407535
|
--- !u!1001 &1326407535
|
||||||
PrefabInstance:
|
PrefabInstance:
|
||||||
|
|
@ -165537,8 +165713,8 @@ Transform:
|
||||||
m_GameObject: {fileID: 1550577165}
|
m_GameObject: {fileID: 1550577165}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0.002655249, y: -0.03898664, z: -0.72380346, w: -0.68889886}
|
m_LocalRotation: {x: 0.002655249, y: -0.03898664, z: -0.72380346, w: -0.68889886}
|
||||||
m_LocalPosition: {x: 0.00351, y: 0.0007, z: 0.00066}
|
m_LocalPosition: {x: 0.00886, y: -0.00478, z: 0.0054}
|
||||||
m_LocalScale: {x: 0.0090075, y: 0.009007501, z: 0.0090075}
|
m_LocalScale: {x: 0.0055073895, y: 0.0055073905, z: 0.0055073895}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 1325714516}
|
m_Father: {fileID: 1325714516}
|
||||||
|
|
@ -228647,6 +228823,22 @@ PrefabInstance:
|
||||||
propertyPath: minLength
|
propertyPath: minLength
|
||||||
value: 0.2
|
value: 0.2
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 8160405634931995834, guid: c34da37720b95c84887eda34e2d90e5b, type: 3}
|
||||||
|
propertyPath: playerStats
|
||||||
|
value:
|
||||||
|
objectReference: {fileID: 1432447530}
|
||||||
|
- target: {fileID: 8160405634931995834, guid: c34da37720b95c84887eda34e2d90e5b, type: 3}
|
||||||
|
propertyPath: OnDieEvent.m_PersistentCalls.m_Calls.Array.size
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 8160405634931995834, guid: c34da37720b95c84887eda34e2d90e5b, type: 3}
|
||||||
|
propertyPath: OnDieEvent.m_PersistentCalls.m_Calls.Array.data[0].m_Target
|
||||||
|
value:
|
||||||
|
objectReference: {fileID: 3957690915496532953}
|
||||||
|
- target: {fileID: 8160405634931995834, guid: c34da37720b95c84887eda34e2d90e5b, type: 3}
|
||||||
|
propertyPath: OnDieEvent.m_PersistentCalls.m_Calls.Array.data[0].m_CallState
|
||||||
|
value: 2
|
||||||
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 8345435216483992478, guid: c34da37720b95c84887eda34e2d90e5b, type: 3}
|
- target: {fileID: 8345435216483992478, guid: c34da37720b95c84887eda34e2d90e5b, type: 3}
|
||||||
propertyPath: m_SlopeLimit
|
propertyPath: m_SlopeLimit
|
||||||
value: 47.82
|
value: 47.82
|
||||||
|
|
@ -228659,6 +228851,10 @@ PrefabInstance:
|
||||||
propertyPath: normalRange
|
propertyPath: normalRange
|
||||||
value: 15
|
value: 15
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 8965661853020836870, guid: c34da37720b95c84887eda34e2d90e5b, type: 3}
|
||||||
|
propertyPath: playerStats
|
||||||
|
value:
|
||||||
|
objectReference: {fileID: 1432447530}
|
||||||
- target: {fileID: 8965661853020836870, guid: c34da37720b95c84887eda34e2d90e5b, type: 3}
|
- target: {fileID: 8965661853020836870, guid: c34da37720b95c84887eda34e2d90e5b, type: 3}
|
||||||
propertyPath: weaponHitBox
|
propertyPath: weaponHitBox
|
||||||
value:
|
value:
|
||||||
|
|
@ -229299,9 +229495,10 @@ SceneRoots:
|
||||||
- {fileID: 2067270637}
|
- {fileID: 2067270637}
|
||||||
- {fileID: 615584948}
|
- {fileID: 615584948}
|
||||||
- {fileID: 200768984}
|
- {fileID: 200768984}
|
||||||
- {fileID: 1325714516}
|
|
||||||
- {fileID: 1759137305}
|
- {fileID: 1759137305}
|
||||||
- {fileID: 198901434}
|
- {fileID: 198901434}
|
||||||
- {fileID: 929255795}
|
- {fileID: 929255795}
|
||||||
- {fileID: 1844125250}
|
- {fileID: 1844125250}
|
||||||
- {fileID: 1101415473}
|
- {fileID: 1101415473}
|
||||||
|
- {fileID: 1081393079}
|
||||||
|
- {fileID: 48215575}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,13 @@ AnimatorController:
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_Name: BossMonsterAnimater
|
m_Name: BossMonsterAnimater
|
||||||
serializedVersion: 5
|
serializedVersion: 5
|
||||||
m_AnimatorParameters: []
|
m_AnimatorParameters:
|
||||||
|
- m_Name: Speed
|
||||||
|
m_Type: 1
|
||||||
|
m_DefaultFloat: 0
|
||||||
|
m_DefaultInt: 0
|
||||||
|
m_DefaultBool: 0
|
||||||
|
m_Controller: {fileID: 9100000}
|
||||||
m_AnimatorLayers:
|
m_AnimatorLayers:
|
||||||
- serializedVersion: 5
|
- serializedVersion: 5
|
||||||
m_Name: Base Layer
|
m_Name: Base Layer
|
||||||
|
|
|
||||||
|
|
@ -1231,6 +1231,9 @@ PrefabInstance:
|
||||||
insertIndex: -1
|
insertIndex: -1
|
||||||
addedObject: {fileID: 7115393052906583336}
|
addedObject: {fileID: 7115393052906583336}
|
||||||
m_AddedComponents:
|
m_AddedComponents:
|
||||||
|
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
|
||||||
|
insertIndex: -1
|
||||||
|
addedObject: {fileID: 8160405634931995834}
|
||||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
|
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
|
||||||
insertIndex: -1
|
insertIndex: -1
|
||||||
addedObject: {fileID: -903076858900737616}
|
addedObject: {fileID: -903076858900737616}
|
||||||
|
|
@ -1243,9 +1246,6 @@ PrefabInstance:
|
||||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
|
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
|
||||||
insertIndex: -1
|
insertIndex: -1
|
||||||
addedObject: {fileID: 8965661853020836870}
|
addedObject: {fileID: 8965661853020836870}
|
||||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
|
|
||||||
insertIndex: -1
|
|
||||||
addedObject: {fileID: 8160405634931995834}
|
|
||||||
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
|
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
|
||||||
insertIndex: -1
|
insertIndex: -1
|
||||||
addedObject: {fileID: 7686364893196566162}
|
addedObject: {fileID: 7686364893196566162}
|
||||||
|
|
@ -1273,6 +1273,28 @@ GameObject:
|
||||||
m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
|
m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
|
||||||
m_PrefabInstance: {fileID: 1112926104530361804}
|
m_PrefabInstance: {fileID: 1112926104530361804}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
--- !u!114 &8160405634931995834
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 265854990890279069}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 21175cd26e6d59449b495b92d2dbb4ce, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier:
|
||||||
|
playerStats: {fileID: 395629277865203624}
|
||||||
|
invincibleDuration: 1
|
||||||
|
OnDieEvent:
|
||||||
|
m_PersistentCalls:
|
||||||
|
m_Calls: []
|
||||||
|
OnHealthChanged:
|
||||||
|
m_PersistentCalls:
|
||||||
|
m_Calls: []
|
||||||
|
isInvincible: 0
|
||||||
|
isHit: 0
|
||||||
--- !u!114 &-903076858900737616
|
--- !u!114 &-903076858900737616
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -1368,23 +1390,6 @@ MonoBehaviour:
|
||||||
m_Bits: 0
|
m_Bits: 0
|
||||||
onlyMaxCharge: 1
|
onlyMaxCharge: 1
|
||||||
defaultProjectilePrefab: {fileID: 0}
|
defaultProjectilePrefab: {fileID: 0}
|
||||||
--- !u!114 &8160405634931995834
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 265854990890279069}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 21175cd26e6d59449b495b92d2dbb4ce, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
stats: {fileID: 395629277865203624}
|
|
||||||
animator: {fileID: 6781151982184439901}
|
|
||||||
attackScript: {fileID: 0}
|
|
||||||
bossPattern: {fileID: 0}
|
|
||||||
isInvincible: 0
|
|
||||||
--- !u!114 &7686364893196566162
|
--- !u!114 &7686364893196566162
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
|
||||||
BIN
Assets/3D Assets/BossKK.fbx
Normal file
BIN
Assets/3D Assets/BossKK.fbx
Normal file
Binary file not shown.
109
Assets/3D Assets/BossKK.fbx.meta
Normal file
109
Assets/3D Assets/BossKK.fbx.meta
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 930e80507da91c14789fbf72fea30274
|
||||||
|
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: []
|
||||||
|
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: 1
|
||||||
|
skeletonHasParents: 1
|
||||||
|
lastHumanDescriptionAvatarSource: {instanceID: 0}
|
||||||
|
autoGenerateAvatarMappingIfUnspecified: 1
|
||||||
|
animationType: 3
|
||||||
|
humanoidOversampling: 1
|
||||||
|
avatarSetup: 1
|
||||||
|
addHumanoidExtraRootOnlyWhenUsingAvatar: 1
|
||||||
|
importBlendShapeDeformPercent: 1
|
||||||
|
remapMaterialsIfMaterialImportModeIsNone: 0
|
||||||
|
additionalBone: 0
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -16,8 +16,7 @@ ModelImporter:
|
||||||
optimizeGameObjects: 0
|
optimizeGameObjects: 0
|
||||||
removeConstantScaleCurves: 0
|
removeConstantScaleCurves: 0
|
||||||
motionNodeName:
|
motionNodeName:
|
||||||
rigImportErrors: "Copied Avatar Rig Configuration mis-match. Transform hierarchy
|
rigImportErrors:
|
||||||
does not match:\n\tTransform 'spine' for human bone 'Hips' not found\n"
|
|
||||||
rigImportWarnings:
|
rigImportWarnings:
|
||||||
animationImportErrors:
|
animationImportErrors:
|
||||||
animationImportWarnings:
|
animationImportWarnings:
|
||||||
|
|
@ -32,7 +31,36 @@ ModelImporter:
|
||||||
animationWrapMode: 0
|
animationWrapMode: 0
|
||||||
extraExposedTransformPaths: []
|
extraExposedTransformPaths: []
|
||||||
extraUserProperties: []
|
extraUserProperties: []
|
||||||
clipAnimations: []
|
clipAnimations:
|
||||||
|
- serializedVersion: 16
|
||||||
|
name: mixamo.com
|
||||||
|
takeName: mixamo.com
|
||||||
|
internalID: -203655887218126122
|
||||||
|
firstFrame: 0
|
||||||
|
lastFrame: 241
|
||||||
|
wrapMode: 0
|
||||||
|
orientationOffsetY: 0
|
||||||
|
level: 0
|
||||||
|
cycleOffset: 0
|
||||||
|
loop: 0
|
||||||
|
hasAdditiveReferencePose: 0
|
||||||
|
loopTime: 1
|
||||||
|
loopBlend: 0
|
||||||
|
loopBlendOrientation: 0
|
||||||
|
loopBlendPositionY: 0
|
||||||
|
loopBlendPositionXZ: 0
|
||||||
|
keepOriginalOrientation: 0
|
||||||
|
keepOriginalPositionY: 1
|
||||||
|
keepOriginalPositionXZ: 0
|
||||||
|
heightFromFeet: 0
|
||||||
|
mirror: 0
|
||||||
|
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
|
||||||
|
curves: []
|
||||||
|
events: []
|
||||||
|
transformMask: []
|
||||||
|
maskType: 3
|
||||||
|
maskSource: {instanceID: 0}
|
||||||
|
additiveReferencePoseFrame: 0
|
||||||
isReadable: 0
|
isReadable: 0
|
||||||
meshes:
|
meshes:
|
||||||
lODScreenPercentages: []
|
lODScreenPercentages: []
|
||||||
|
|
@ -82,8 +110,402 @@ ModelImporter:
|
||||||
importAnimation: 1
|
importAnimation: 1
|
||||||
humanDescription:
|
humanDescription:
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
human: []
|
human:
|
||||||
skeleton: []
|
- boneName: mixamorig:Hips
|
||||||
|
humanName: Hips
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftUpLeg
|
||||||
|
humanName: LeftUpperLeg
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightUpLeg
|
||||||
|
humanName: RightUpperLeg
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftLeg
|
||||||
|
humanName: LeftLowerLeg
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightLeg
|
||||||
|
humanName: RightLowerLeg
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftFoot
|
||||||
|
humanName: LeftFoot
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightFoot
|
||||||
|
humanName: RightFoot
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:Spine
|
||||||
|
humanName: Spine
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:Spine1
|
||||||
|
humanName: Chest
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:Neck
|
||||||
|
humanName: Neck
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:Head
|
||||||
|
humanName: Head
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftShoulder
|
||||||
|
humanName: LeftShoulder
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightShoulder
|
||||||
|
humanName: RightShoulder
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftArm
|
||||||
|
humanName: LeftUpperArm
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightArm
|
||||||
|
humanName: RightUpperArm
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftForeArm
|
||||||
|
humanName: LeftLowerArm
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightForeArm
|
||||||
|
humanName: RightLowerArm
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftHand
|
||||||
|
humanName: LeftHand
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightHand
|
||||||
|
humanName: RightHand
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftToeBase
|
||||||
|
humanName: LeftToes
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightToeBase
|
||||||
|
humanName: RightToes
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftHandIndex1
|
||||||
|
humanName: Left Index Proximal
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftHandIndex2
|
||||||
|
humanName: Left Index Intermediate
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftHandIndex3
|
||||||
|
humanName: Left Index Distal
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightHandIndex1
|
||||||
|
humanName: Right Index Proximal
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightHandIndex2
|
||||||
|
humanName: Right Index Intermediate
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightHandIndex3
|
||||||
|
humanName: Right Index Distal
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:Spine2
|
||||||
|
humanName: UpperChest
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
skeleton:
|
||||||
|
- name: BossIdle(Clone)
|
||||||
|
parentName:
|
||||||
|
position: {x: 0, y: 0, z: 0}
|
||||||
|
rotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:Hips
|
||||||
|
parentName: BossIdle(Clone)
|
||||||
|
position: {x: 0.0026833098, y: 0.9943079, z: -0.012676053}
|
||||||
|
rotation: {x: -0.002174467, y: -0.0005086692, z: 0.0057661077, w: 0.9999809}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:Spine
|
||||||
|
parentName: mixamorig:Hips
|
||||||
|
position: {x: -0, y: 0.1171, z: 0.0029}
|
||||||
|
rotation: {x: 0.1565307, y: -0.00068456325, z: 0.000030100546, w: 0.98767287}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:Spine1
|
||||||
|
parentName: mixamorig:Spine
|
||||||
|
position: {x: -0, y: 0.1367, z: 0}
|
||||||
|
rotation: {x: 0.09012192, y: -0.0022969642, z: -0.007762773, w: 0.9958978}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:Spine2
|
||||||
|
parentName: mixamorig:Spine1
|
||||||
|
position: {x: -0, y: 0.1562, z: 0}
|
||||||
|
rotation: {x: -0.0013413654, y: -0.0019386135, z: -0.0049632005, w: 0.9999849}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:Neck
|
||||||
|
parentName: mixamorig:Spine2
|
||||||
|
position: {x: -0, y: 0.1758, z: 0}
|
||||||
|
rotation: {x: -0.37416202, y: 0.0052613267, z: 0.0064393664, w: 0.927326}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:Head
|
||||||
|
parentName: mixamorig:Neck
|
||||||
|
position: {x: -0, y: 0.0536, z: 0.0305}
|
||||||
|
rotation: {x: -0.18870564, y: -0.0236753, z: 0.017435722, w: 0.98159343}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:HeadTop_End
|
||||||
|
parentName: mixamorig:Head
|
||||||
|
position: {x: -0, y: 0.1727, z: 0.0982}
|
||||||
|
rotation: {x: 0, y: -0, z: -0, w: 1}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftShoulder
|
||||||
|
parentName: mixamorig:Spine2
|
||||||
|
position: {x: -0.064, y: 0.1358, z: -0.0078}
|
||||||
|
rotation: {x: 0.6866209, y: -0.46074542, z: 0.44875145, w: 0.33895078}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftArm
|
||||||
|
parentName: mixamorig:LeftShoulder
|
||||||
|
position: {x: -0, y: 0.1556, z: 0}
|
||||||
|
rotation: {x: -0.13769072, y: -0.0002792626, z: 0.060832813, w: 0.9886054}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftForeArm
|
||||||
|
parentName: mixamorig:LeftArm
|
||||||
|
position: {x: -0, y: 0.2133, z: 0}
|
||||||
|
rotation: {x: -0.005401833, y: -0.06163869, z: 0.0028284462, w: 0.99807996}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftHand
|
||||||
|
parentName: mixamorig:LeftForeArm
|
||||||
|
position: {x: -0, y: 0.3139, z: 0}
|
||||||
|
rotation: {x: -0.06714013, y: 0.43071648, z: -0.18811981, w: 0.8801059}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftHandIndex1
|
||||||
|
parentName: mixamorig:LeftHand
|
||||||
|
position: {x: -0, y: 0.0657, z: 0}
|
||||||
|
rotation: {x: 0.05516846, y: -0.65295166, z: -0.05171242, w: 0.75361556}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftHandIndex2
|
||||||
|
parentName: mixamorig:LeftHandIndex1
|
||||||
|
position: {x: -0, y: 0.0705, z: 0}
|
||||||
|
rotation: {x: 0.3534335, y: -0.012413347, z: -0.101619236, w: 0.929841}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftHandIndex3
|
||||||
|
parentName: mixamorig:LeftHandIndex2
|
||||||
|
position: {x: -0.0092, y: 0.0512, z: -0.0468}
|
||||||
|
rotation: {x: -0.13607779, y: 0.1358576, z: 0.17146648, w: 0.9662426}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftHandIndex4
|
||||||
|
parentName: mixamorig:LeftHandIndex3
|
||||||
|
position: {x: 0.0269, y: 0.0618, z: -0.0313}
|
||||||
|
rotation: {x: 0, y: -0, z: -0, w: 1}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightShoulder
|
||||||
|
parentName: mixamorig:Spine2
|
||||||
|
position: {x: 0.064, y: 0.136, z: 0.0016}
|
||||||
|
rotation: {x: 0.67744666, y: 0.47600952, z: -0.42826772, w: 0.36203295}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightArm
|
||||||
|
parentName: mixamorig:RightShoulder
|
||||||
|
position: {x: -0, y: 0.1546, z: 0}
|
||||||
|
rotation: {x: -0.13277586, y: -0.21806774, z: -0.04718864, w: 0.9657072}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightForeArm
|
||||||
|
parentName: mixamorig:RightArm
|
||||||
|
position: {x: -0, y: 0.2128, z: 0}
|
||||||
|
rotation: {x: -0.017916152, y: -0.14330766, z: 0.0014052424, w: 0.989515}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightHand
|
||||||
|
parentName: mixamorig:RightForeArm
|
||||||
|
position: {x: -0, y: 0.3137, z: 0}
|
||||||
|
rotation: {x: 0.0841442, y: -0.13422275, z: -0.006523322, w: 0.9873507}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightHandIndex1
|
||||||
|
parentName: mixamorig:RightHand
|
||||||
|
position: {x: -0, y: 0.2098, z: 0}
|
||||||
|
rotation: {x: 0.06868836, y: 0.15733252, z: -0.014478484, w: 0.98504764}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightHandIndex2
|
||||||
|
parentName: mixamorig:RightHandIndex1
|
||||||
|
position: {x: -0, y: 0.0172, z: 0}
|
||||||
|
rotation: {x: -0.07102833, y: -0.010314832, z: -0.035351228, w: 0.99679434}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightHandIndex3
|
||||||
|
parentName: mixamorig:RightHandIndex2
|
||||||
|
position: {x: -0.0018, y: 0.0213, z: 0.0043}
|
||||||
|
rotation: {x: 0.07075478, y: -0.04816231, z: 0.077679224, w: 0.99329764}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightHandIndex4
|
||||||
|
parentName: mixamorig:RightHandIndex3
|
||||||
|
position: {x: 0.0007, y: 0.0188, z: 0.0021}
|
||||||
|
rotation: {x: 0, y: -0, z: -0, w: 1}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftUpLeg
|
||||||
|
parentName: mixamorig:Hips
|
||||||
|
position: {x: -0.0906, y: -0.0651, z: 0.0026}
|
||||||
|
rotation: {x: -0.13512881, y: 0.07888633, z: 0.9815783, w: 0.10964156}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftLeg
|
||||||
|
parentName: mixamorig:LeftUpLeg
|
||||||
|
position: {x: -0, y: 0.392, z: 0}
|
||||||
|
rotation: {x: -0.08549402, y: -0.67984086, z: 0.29791063, w: 0.6646476}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftFoot
|
||||||
|
parentName: mixamorig:LeftLeg
|
||||||
|
position: {x: -0, y: 0.4038, z: 0}
|
||||||
|
rotation: {x: 0.37818304, y: 0.57879406, z: -0.44982204, w: 0.5653628}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftToeBase
|
||||||
|
parentName: mixamorig:LeftFoot
|
||||||
|
position: {x: -0, y: 0.2346, z: 0}
|
||||||
|
rotation: {x: 0.3567435, y: -0.07525214, z: 0.12925003, w: 0.92215276}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftToe_End
|
||||||
|
parentName: mixamorig:LeftToeBase
|
||||||
|
position: {x: -0, y: 0.1139, z: 0}
|
||||||
|
rotation: {x: 0, y: -0, z: -0, w: 1}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightUpLeg
|
||||||
|
parentName: mixamorig:Hips
|
||||||
|
position: {x: 0.0906, y: -0.0651, z: 0.0037}
|
||||||
|
rotation: {x: 0.21856742, y: 0.09107618, z: 0.9678308, w: -0.08507111}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightLeg
|
||||||
|
parentName: mixamorig:RightUpLeg
|
||||||
|
position: {x: -0, y: 0.3914, z: 0}
|
||||||
|
rotation: {x: -0.29833734, y: -0.49995032, z: 0.099081814, w: 0.8069866}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightFoot
|
||||||
|
parentName: mixamorig:RightLeg
|
||||||
|
position: {x: -0, y: 0.4064, z: 0}
|
||||||
|
rotation: {x: 0.4983791, y: 0.46412635, z: -0.31939918, w: 0.6589303}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightToeBase
|
||||||
|
parentName: mixamorig:RightFoot
|
||||||
|
position: {x: -0, y: 0.2331, z: 0}
|
||||||
|
rotation: {x: 0.3513939, y: -0.029390866, z: -0.13634582, w: 0.9257798}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightToe_End
|
||||||
|
parentName: mixamorig:RightToeBase
|
||||||
|
position: {x: -0, y: 0.1125, z: 0}
|
||||||
|
rotation: {x: 0, y: -0, z: -0, w: 1}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
armTwist: 0.5
|
armTwist: 0.5
|
||||||
foreArmTwist: 0.5
|
foreArmTwist: 0.5
|
||||||
upperLegTwist: 0.5
|
upperLegTwist: 0.5
|
||||||
|
|
|
||||||
95
Assets/5.Enemy Animation/BossAnimation'/BossKK.controller
Normal file
95
Assets/5.Enemy Animation/BossAnimation'/BossKK.controller
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!1101 &-5377988278109688191
|
||||||
|
AnimatorStateTransition:
|
||||||
|
m_ObjectHideFlags: 1
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name:
|
||||||
|
m_Conditions: []
|
||||||
|
m_DstStateMachine: {fileID: 0}
|
||||||
|
m_DstState: {fileID: 0}
|
||||||
|
m_Solo: 0
|
||||||
|
m_Mute: 0
|
||||||
|
m_IsExit: 1
|
||||||
|
serializedVersion: 3
|
||||||
|
m_TransitionDuration: 0.25
|
||||||
|
m_TransitionOffset: 0
|
||||||
|
m_ExitTime: 0.93775934
|
||||||
|
m_HasExitTime: 1
|
||||||
|
m_HasFixedDuration: 1
|
||||||
|
m_InterruptionSource: 0
|
||||||
|
m_OrderedInterruption: 1
|
||||||
|
m_CanTransitionToSelf: 1
|
||||||
|
--- !u!1107 &-3325615192832722807
|
||||||
|
AnimatorStateMachine:
|
||||||
|
serializedVersion: 6
|
||||||
|
m_ObjectHideFlags: 1
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name: Base Layer
|
||||||
|
m_ChildStates:
|
||||||
|
- serializedVersion: 1
|
||||||
|
m_State: {fileID: 5540596969216719168}
|
||||||
|
m_Position: {x: 320, y: 20, z: 0}
|
||||||
|
m_ChildStateMachines: []
|
||||||
|
m_AnyStateTransitions: []
|
||||||
|
m_EntryTransitions: []
|
||||||
|
m_StateMachineTransitions: {}
|
||||||
|
m_StateMachineBehaviours: []
|
||||||
|
m_AnyStatePosition: {x: 50, y: 20, z: 0}
|
||||||
|
m_EntryPosition: {x: 50, y: 120, z: 0}
|
||||||
|
m_ExitPosition: {x: 800, y: 120, z: 0}
|
||||||
|
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
|
||||||
|
m_DefaultState: {fileID: 5540596969216719168}
|
||||||
|
--- !u!91 &9100000
|
||||||
|
AnimatorController:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name: BossKK
|
||||||
|
serializedVersion: 5
|
||||||
|
m_AnimatorParameters: []
|
||||||
|
m_AnimatorLayers:
|
||||||
|
- serializedVersion: 5
|
||||||
|
m_Name: Base Layer
|
||||||
|
m_StateMachine: {fileID: -3325615192832722807}
|
||||||
|
m_Mask: {fileID: 0}
|
||||||
|
m_Motions: []
|
||||||
|
m_Behaviours: []
|
||||||
|
m_BlendingMode: 0
|
||||||
|
m_SyncedLayerIndex: -1
|
||||||
|
m_DefaultWeight: 0
|
||||||
|
m_IKPass: 0
|
||||||
|
m_SyncedLayerAffectsTiming: 0
|
||||||
|
m_Controller: {fileID: 9100000}
|
||||||
|
--- !u!1102 &5540596969216719168
|
||||||
|
AnimatorState:
|
||||||
|
serializedVersion: 6
|
||||||
|
m_ObjectHideFlags: 1
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name: mixamo_com
|
||||||
|
m_Speed: 1
|
||||||
|
m_CycleOffset: 0
|
||||||
|
m_Transitions:
|
||||||
|
- {fileID: -5377988278109688191}
|
||||||
|
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: -203655887218126122, guid: dc29be49c4c60f74899f0410ec955195, type: 3}
|
||||||
|
m_Tag:
|
||||||
|
m_SpeedParameter:
|
||||||
|
m_MirrorParameter:
|
||||||
|
m_CycleOffsetParameter:
|
||||||
|
m_TimeParameter:
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bff4e8c104f01194fb159c6c309b3a49
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 9100000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -16,8 +16,7 @@ ModelImporter:
|
||||||
optimizeGameObjects: 0
|
optimizeGameObjects: 0
|
||||||
removeConstantScaleCurves: 0
|
removeConstantScaleCurves: 0
|
||||||
motionNodeName:
|
motionNodeName:
|
||||||
rigImportErrors: "Copied Avatar Rig Configuration mis-match. Transform hierarchy
|
rigImportErrors:
|
||||||
does not match:\n\tTransform 'spine' for human bone 'Hips' not found\n"
|
|
||||||
rigImportWarnings:
|
rigImportWarnings:
|
||||||
animationImportErrors:
|
animationImportErrors:
|
||||||
animationImportWarnings:
|
animationImportWarnings:
|
||||||
|
|
@ -32,7 +31,36 @@ ModelImporter:
|
||||||
animationWrapMode: 0
|
animationWrapMode: 0
|
||||||
extraExposedTransformPaths: []
|
extraExposedTransformPaths: []
|
||||||
extraUserProperties: []
|
extraUserProperties: []
|
||||||
clipAnimations: []
|
clipAnimations:
|
||||||
|
- serializedVersion: 16
|
||||||
|
name: mixamo.com
|
||||||
|
takeName: mixamo.com
|
||||||
|
internalID: -203655887218126122
|
||||||
|
firstFrame: 0
|
||||||
|
lastFrame: 85
|
||||||
|
wrapMode: 0
|
||||||
|
orientationOffsetY: 0
|
||||||
|
level: 0
|
||||||
|
cycleOffset: 0
|
||||||
|
loop: 0
|
||||||
|
hasAdditiveReferencePose: 0
|
||||||
|
loopTime: 1
|
||||||
|
loopBlend: 0
|
||||||
|
loopBlendOrientation: 0
|
||||||
|
loopBlendPositionY: 0
|
||||||
|
loopBlendPositionXZ: 0
|
||||||
|
keepOriginalOrientation: 0
|
||||||
|
keepOriginalPositionY: 1
|
||||||
|
keepOriginalPositionXZ: 0
|
||||||
|
heightFromFeet: 0
|
||||||
|
mirror: 0
|
||||||
|
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
|
||||||
|
curves: []
|
||||||
|
events: []
|
||||||
|
transformMask: []
|
||||||
|
maskType: 3
|
||||||
|
maskSource: {instanceID: 0}
|
||||||
|
additiveReferencePoseFrame: 0
|
||||||
isReadable: 0
|
isReadable: 0
|
||||||
meshes:
|
meshes:
|
||||||
lODScreenPercentages: []
|
lODScreenPercentages: []
|
||||||
|
|
@ -82,8 +110,402 @@ ModelImporter:
|
||||||
importAnimation: 1
|
importAnimation: 1
|
||||||
humanDescription:
|
humanDescription:
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
human: []
|
human:
|
||||||
skeleton: []
|
- boneName: mixamorig:Hips
|
||||||
|
humanName: Hips
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftUpLeg
|
||||||
|
humanName: LeftUpperLeg
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightUpLeg
|
||||||
|
humanName: RightUpperLeg
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftLeg
|
||||||
|
humanName: LeftLowerLeg
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightLeg
|
||||||
|
humanName: RightLowerLeg
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftFoot
|
||||||
|
humanName: LeftFoot
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightFoot
|
||||||
|
humanName: RightFoot
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:Spine
|
||||||
|
humanName: Spine
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:Spine1
|
||||||
|
humanName: Chest
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:Neck
|
||||||
|
humanName: Neck
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:Head
|
||||||
|
humanName: Head
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftShoulder
|
||||||
|
humanName: LeftShoulder
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightShoulder
|
||||||
|
humanName: RightShoulder
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftArm
|
||||||
|
humanName: LeftUpperArm
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightArm
|
||||||
|
humanName: RightUpperArm
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftForeArm
|
||||||
|
humanName: LeftLowerArm
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightForeArm
|
||||||
|
humanName: RightLowerArm
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftHand
|
||||||
|
humanName: LeftHand
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightHand
|
||||||
|
humanName: RightHand
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftToeBase
|
||||||
|
humanName: LeftToes
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightToeBase
|
||||||
|
humanName: RightToes
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftHandIndex1
|
||||||
|
humanName: Left Index Proximal
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftHandIndex2
|
||||||
|
humanName: Left Index Intermediate
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:LeftHandIndex3
|
||||||
|
humanName: Left Index Distal
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightHandIndex1
|
||||||
|
humanName: Right Index Proximal
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightHandIndex2
|
||||||
|
humanName: Right Index Intermediate
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:RightHandIndex3
|
||||||
|
humanName: Right Index Distal
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
- boneName: mixamorig:Spine2
|
||||||
|
humanName: UpperChest
|
||||||
|
limit:
|
||||||
|
min: {x: 0, y: 0, z: 0}
|
||||||
|
max: {x: 0, y: 0, z: 0}
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
length: 0
|
||||||
|
modified: 0
|
||||||
|
skeleton:
|
||||||
|
- name: BossWalk(Clone)
|
||||||
|
parentName:
|
||||||
|
position: {x: 0, y: 0, z: 0}
|
||||||
|
rotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:Hips
|
||||||
|
parentName: BossWalk(Clone)
|
||||||
|
position: {x: 0.0025809966, y: 0.9786751, z: -0.010516135}
|
||||||
|
rotation: {x: -0.06791151, y: 0.046682756, z: -0.025422283, w: 0.9962743}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:Spine
|
||||||
|
parentName: mixamorig:Hips
|
||||||
|
position: {x: -0, y: 0.1171, z: 0.0029}
|
||||||
|
rotation: {x: 0.052984815, y: -0.010016891, z: 0.016819054, w: 0.99840343}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:Spine1
|
||||||
|
parentName: mixamorig:Spine
|
||||||
|
position: {x: -0, y: 0.1367, z: 0}
|
||||||
|
rotation: {x: 0.08616688, y: -0.018176258, z: 0.023615148, w: 0.99583495}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:Spine2
|
||||||
|
parentName: mixamorig:Spine1
|
||||||
|
position: {x: -0, y: 0.1562, z: 0}
|
||||||
|
rotation: {x: 0.08652799, y: -0.016902944, z: 0.023167178, w: 0.99583656}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:Neck
|
||||||
|
parentName: mixamorig:Spine2
|
||||||
|
position: {x: -0, y: 0.1758, z: 0}
|
||||||
|
rotation: {x: -0.17461915, y: 0.03961615, z: -0.02823548, w: 0.9834335}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:Head
|
||||||
|
parentName: mixamorig:Neck
|
||||||
|
position: {x: -0, y: 0.0536, z: 0.0305}
|
||||||
|
rotation: {x: -0.18009952, y: 0.088830575, z: 0.009434662, w: 0.97958374}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:HeadTop_End
|
||||||
|
parentName: mixamorig:Head
|
||||||
|
position: {x: -0, y: 0.1727, z: 0.0982}
|
||||||
|
rotation: {x: 0, y: -0, z: -0, w: 1}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftShoulder
|
||||||
|
parentName: mixamorig:Spine2
|
||||||
|
position: {x: -0.064, y: 0.1358, z: -0.0078}
|
||||||
|
rotation: {x: 0.54345345, y: -0.44411075, z: 0.5735583, w: 0.4224394}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftArm
|
||||||
|
parentName: mixamorig:LeftShoulder
|
||||||
|
position: {x: -0, y: 0.1556, z: 0}
|
||||||
|
rotation: {x: -0.14027706, y: -0.238704, z: -0.008420287, w: 0.96087044}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftForeArm
|
||||||
|
parentName: mixamorig:LeftArm
|
||||||
|
position: {x: -0, y: 0.2133, z: 0}
|
||||||
|
rotation: {x: -0.016032558, y: 0.018755268, z: 0.011540319, w: 0.999629}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftHand
|
||||||
|
parentName: mixamorig:LeftForeArm
|
||||||
|
position: {x: -0, y: 0.3139, z: 0}
|
||||||
|
rotation: {x: 0.016468085, y: 0.11859691, z: -0.113853276, w: 0.98625606}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftHandIndex1
|
||||||
|
parentName: mixamorig:LeftHand
|
||||||
|
position: {x: -0, y: 0.0657, z: 0}
|
||||||
|
rotation: {x: 0.043096147, y: -0.6442412, z: -0.05172926, w: 0.76185304}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftHandIndex2
|
||||||
|
parentName: mixamorig:LeftHandIndex1
|
||||||
|
position: {x: -0, y: 0.0705, z: 0}
|
||||||
|
rotation: {x: 0.35360065, y: -0.012077208, z: -0.100781396, w: 0.92987305}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftHandIndex3
|
||||||
|
parentName: mixamorig:LeftHandIndex2
|
||||||
|
position: {x: -0.0092, y: 0.0512, z: -0.0468}
|
||||||
|
rotation: {x: -0.1376931, y: 0.13756303, z: 0.16533016, w: 0.96684176}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftHandIndex4
|
||||||
|
parentName: mixamorig:LeftHandIndex3
|
||||||
|
position: {x: 0.0269, y: 0.0618, z: -0.0313}
|
||||||
|
rotation: {x: 0, y: -0, z: -0, w: 1}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightShoulder
|
||||||
|
parentName: mixamorig:Spine2
|
||||||
|
position: {x: 0.064, y: 0.136, z: 0.0016}
|
||||||
|
rotation: {x: 0.60436624, y: 0.39707544, z: -0.5637547, w: 0.3990654}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightArm
|
||||||
|
parentName: mixamorig:RightShoulder
|
||||||
|
position: {x: -0, y: 0.1546, z: 0}
|
||||||
|
rotation: {x: -0.12594903, y: 0.034397986, z: -0.037340727, w: 0.9907368}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightForeArm
|
||||||
|
parentName: mixamorig:RightArm
|
||||||
|
position: {x: -0, y: 0.2128, z: 0}
|
||||||
|
rotation: {x: -0.027492605, y: -0.060033023, z: 0.02598758, w: 0.9974792}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightHand
|
||||||
|
parentName: mixamorig:RightForeArm
|
||||||
|
position: {x: -0, y: 0.3137, z: 0}
|
||||||
|
rotation: {x: 0.084213026, y: -0.1548261, z: -0.020946275, w: 0.9841231}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightHandIndex1
|
||||||
|
parentName: mixamorig:RightHand
|
||||||
|
position: {x: -0, y: 0.2098, z: 0}
|
||||||
|
rotation: {x: 0.064756036, y: -0.025416251, z: -0.020036412, w: 0.9973762}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightHandIndex2
|
||||||
|
parentName: mixamorig:RightHandIndex1
|
||||||
|
position: {x: -0, y: 0.0172, z: 0}
|
||||||
|
rotation: {x: -0.066692896, y: 0.001831721, z: -0.046279315, w: 0.9966981}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightHandIndex3
|
||||||
|
parentName: mixamorig:RightHandIndex2
|
||||||
|
position: {x: -0.0018, y: 0.0213, z: 0.0043}
|
||||||
|
rotation: {x: 0.07979187, y: -0.0087349545, z: 0.05717984, w: 0.9951319}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightHandIndex4
|
||||||
|
parentName: mixamorig:RightHandIndex3
|
||||||
|
position: {x: 0.0007, y: 0.0188, z: 0.0021}
|
||||||
|
rotation: {x: 0, y: -0, z: -0, w: 1}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftUpLeg
|
||||||
|
parentName: mixamorig:Hips
|
||||||
|
position: {x: -0.0906, y: -0.0651, z: 0.0026}
|
||||||
|
rotation: {x: -0.092424944, y: 0.04025192, z: 0.9944845, w: 0.028948884}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftLeg
|
||||||
|
parentName: mixamorig:LeftUpLeg
|
||||||
|
position: {x: -0, y: 0.392, z: 0}
|
||||||
|
rotation: {x: -0.14082664, y: -0.72221404, z: 0.29314968, w: 0.61044085}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftFoot
|
||||||
|
parentName: mixamorig:LeftLeg
|
||||||
|
position: {x: -0, y: 0.4038, z: 0}
|
||||||
|
rotation: {x: 0.3932538, y: 0.6461467, z: -0.377344, w: 0.53428215}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftToeBase
|
||||||
|
parentName: mixamorig:LeftFoot
|
||||||
|
position: {x: -0, y: 0.2346, z: 0}
|
||||||
|
rotation: {x: 0.3386809, y: -0.052146785, z: 0.01728521, w: 0.9392961}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:LeftToe_End
|
||||||
|
parentName: mixamorig:LeftToeBase
|
||||||
|
position: {x: -0, y: 0.1139, z: 0}
|
||||||
|
rotation: {x: 0, y: -0, z: -0, w: 1}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightUpLeg
|
||||||
|
parentName: mixamorig:Hips
|
||||||
|
position: {x: 0.0906, y: -0.0651, z: 0.0037}
|
||||||
|
rotation: {x: 0.10251077, y: 0.036421414, z: 0.98532605, w: -0.1315201}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightLeg
|
||||||
|
parentName: mixamorig:RightUpLeg
|
||||||
|
position: {x: -0, y: 0.3914, z: 0}
|
||||||
|
rotation: {x: -0.29762018, y: -0.6152928, z: 0.0053073, w: 0.7299377}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightFoot
|
||||||
|
parentName: mixamorig:RightLeg
|
||||||
|
position: {x: -0, y: 0.4064, z: 0}
|
||||||
|
rotation: {x: 0.5381931, y: 0.48825583, z: -0.36212963, w: 0.5837951}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightToeBase
|
||||||
|
parentName: mixamorig:RightFoot
|
||||||
|
position: {x: -0, y: 0.2331, z: 0}
|
||||||
|
rotation: {x: 0.33407575, y: 0.04256158, z: -0.014975957, w: 0.9414657}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
|
- name: mixamorig:RightToe_End
|
||||||
|
parentName: mixamorig:RightToeBase
|
||||||
|
position: {x: -0, y: 0.1125, z: 0}
|
||||||
|
rotation: {x: 0, y: -0, z: -0, w: 1}
|
||||||
|
scale: {x: 1, y: 1, z: 1}
|
||||||
armTwist: 0.5
|
armTwist: 0.5
|
||||||
foreArmTwist: 0.5
|
foreArmTwist: 0.5
|
||||||
upperLegTwist: 0.5
|
upperLegTwist: 0.5
|
||||||
|
|
|
||||||
|
|
@ -1,78 +1,80 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 기본 기능(Time, Vector3, Coroutine 등)을 쓰기 위해 불러올거에요 -> UnityEngine를
|
||||||
using Cinemachine;
|
using Cinemachine; // 시네머신 카메라/임펄스 기능을 쓰기 위해 불러올거에요 -> Cinemachine을
|
||||||
using System.Collections;
|
using System.Collections; // 코루틴(IEnumerator)을 쓰기 위해 불러올거에요 -> System.Collections를
|
||||||
|
|
||||||
public class CinemachineShake : MonoBehaviour
|
public class CinemachineShake : MonoBehaviour // 클래스를 선언할거에요 -> 카메라 흔들림/줌/히트슬로우를 담당하는 CinemachineShake를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> CinemachineShake 범위를
|
||||||
public static CinemachineShake Instance { get; private set; }
|
|
||||||
|
|
||||||
[Header("--- 컴포넌트 연결 ---")]
|
public static CinemachineShake Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스를 외부에서 읽기만 가능하게
|
||||||
private CinemachineImpulseSource _impulseSource;
|
|
||||||
[SerializeField] private CinemachineVirtualCamera _vCam;
|
|
||||||
|
|
||||||
[Header("--- 줌 설정 ---")]
|
[Header("--- 컴포넌트 연결 ---")] // 인스펙터에 제목을 표시할거에요 -> 컴포넌트 연결 섹션을
|
||||||
[SerializeField] private float defaultFOV = 60f;
|
private CinemachineImpulseSource _impulseSource; // 변수를 선언할거에요 -> 임펄스(카메라 흔들림) 발사기 컴포넌트를 담을 _impulseSource를
|
||||||
[SerializeField] private float zoomedFOV = 45f;
|
[SerializeField] private CinemachineVirtualCamera _vCam; // 변수를 선언할거에요 -> 조절할 가상 카메라를 담을 _vCam을
|
||||||
[SerializeField] private float zoomSpeed = 5f;
|
|
||||||
|
|
||||||
private Coroutine _zoomCoroutine;
|
[Header("--- 줌 설정 ---")] // 인스펙터에 제목을 표시할거에요 -> 줌 설정 섹션을
|
||||||
|
[SerializeField] private float defaultFOV = 60f; // 변수를 선언할거에요 -> 기본 FOV를 defaultFOV에
|
||||||
|
[SerializeField] private float zoomedFOV = 45f; // 변수를 선언할거에요 -> 줌인 FOV를 zoomedFOV에
|
||||||
|
[SerializeField] private float zoomSpeed = 5f; // 변수를 선언할거에요 -> 줌 보간 속도를 zoomSpeed에
|
||||||
|
|
||||||
private void Awake()
|
private Coroutine _zoomCoroutine; // 변수를 선언할거에요 -> 현재 실행 중인 줌 코루틴을 담을 _zoomCoroutine을
|
||||||
{
|
|
||||||
Instance = this;
|
|
||||||
_impulseSource = GetComponent<CinemachineImpulseSource>();
|
|
||||||
if (_vCam == null) _vCam = GetComponent<CinemachineVirtualCamera>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ⭐ 렉 없는 타격감: 시간을 0.1배속으로 늘림 (Hit-Slow)
|
private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를
|
||||||
public void HitSlow(float duration = 0.15f, float timeScale = 0.1f)
|
{ // 코드 블록을 시작할거에요 -> Awake 범위를
|
||||||
{
|
Instance = this; // 값을 넣을거에요 -> 싱글톤 인스턴스를 자기 자신으로 설정
|
||||||
StartCoroutine(DoHitSlow(duration, timeScale));
|
_impulseSource = GetComponent<CinemachineImpulseSource>(); // 컴포넌트를 가져올거에요 -> 내 오브젝트에서 임펄스 소스를 찾아 저장
|
||||||
}
|
if (_vCam == null) _vCam = GetComponent<CinemachineVirtualCamera>(); // 조건이 맞으면 실행할거에요 -> vCam이 비어있으면 내 오브젝트에서 찾아 채우기
|
||||||
|
} // 코드 블록을 끝낼거에요 -> Awake를
|
||||||
|
|
||||||
private IEnumerator DoHitSlow(float duration, float timeScale)
|
// ⭐ 렉 없는 타격감: 시간을 0.1배속으로 늘림 (Hit-Slow) // 설명을 적을거에요 -> 짧게 슬로모션을 걸어 타격감을 올리는 기능
|
||||||
{
|
public void HitSlow(float duration = 0.15f, float timeScale = 0.1f) // 함수를 선언할거에요 -> duration/timeScale로 히트슬로우를 주는 HitSlow를
|
||||||
Time.timeScale = timeScale; // 멈추지 않고 아주 느리게
|
{ // 코드 블록을 시작할거에요 -> HitSlow 범위를
|
||||||
yield return new WaitForSecondsRealtime(duration);
|
StartCoroutine(DoHitSlow(duration, timeScale)); // 코루틴을 실행할거에요 -> 실제 슬로우 처리를 하는 DoHitSlow를
|
||||||
Time.timeScale = 1.0f;
|
} // 코드 블록을 끝낼거에요 -> HitSlow를
|
||||||
}
|
|
||||||
|
|
||||||
// ⭐ 카메라 킥: 순간적으로 FOV를 확 줄여 충격을 줌
|
private IEnumerator DoHitSlow(float duration, float timeScale) // 코루틴 함수를 선언할거에요 -> 히트슬로우를 수행하는 DoHitSlow를
|
||||||
public void CameraKick(float kickAmount = 8f)
|
{ // 코드 블록을 시작할거에요 -> DoHitSlow 범위를
|
||||||
{
|
Time.timeScale = timeScale; // 값을 바꿀거에요 -> 전체 게임 시간을 느리게(멈춤은 아님)
|
||||||
if (_vCam == null) return;
|
yield return new WaitForSecondsRealtime(duration); // 기다릴거에요 -> 실제 시간 기준으로 duration만큼
|
||||||
_vCam.m_Lens.FieldOfView -= kickAmount; // 순간 줌인
|
Time.timeScale = 1.0f; // 값을 복구할거에요 -> 게임 시간을 정상(1)으로
|
||||||
SetZoom(false); // AnimateZoom을 통해 부드럽게 복구
|
} // 코드 블록을 끝낼거에요 -> DoHitSlow를
|
||||||
}
|
|
||||||
|
|
||||||
public void ShakeAttack()
|
// ⭐ 카메라 킥: 순간적으로 FOV를 확 줄여 충격을 줌 // 설명을 적을거에요 -> 순간 줌인 후 부드럽게 복구시키는 연출
|
||||||
{
|
public void CameraKick(float kickAmount = 8f) // 함수를 선언할거에요 -> FOV를 순간 감소시키는 CameraKick을
|
||||||
if (_impulseSource == null) return;
|
{ // 코드 블록을 시작할거에요 -> CameraKick 범위를
|
||||||
_impulseSource.GenerateImpulse(Vector3.down * 1.5f);
|
if (_vCam == null) return; // 조건이 맞으면 종료할거에요 -> 카메라가 없으면 처리 불가
|
||||||
}
|
_vCam.m_Lens.FieldOfView -= kickAmount; // 값을 바꿀거에요 -> FOV를 줄여서 순간 줌인 효과
|
||||||
|
SetZoom(false); // 함수를 실행할거에요 -> 기본 FOV로 부드럽게 돌아가도록 줌 애니메이션 시작
|
||||||
|
} // 코드 블록을 끝낼거에요 -> CameraKick을
|
||||||
|
|
||||||
public void ShakeNoNo()
|
public void ShakeAttack() // 함수를 선언할거에요 -> 공격용 흔들림을 주는 ShakeAttack을
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> ShakeAttack 범위를
|
||||||
if (_impulseSource == null) return;
|
if (_impulseSource == null) return; // 조건이 맞으면 종료할거에요 -> 임펄스 소스가 없으면 불가
|
||||||
_impulseSource.GenerateImpulse(Vector3.right * 0.4f);
|
_impulseSource.GenerateImpulse(Vector3.down * 1.5f); // 임펄스를 발생시킬거에요 -> 아래 방향으로 강하게 흔들기
|
||||||
}
|
} // 코드 블록을 끝낼거에요 -> ShakeAttack을
|
||||||
|
|
||||||
public void SetZoom(bool isZooming)
|
public void ShakeNoNo() // 함수를 선언할거에요 -> 좌우로 살짝 흔드는 ShakeNoNo를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> ShakeNoNo 범위를
|
||||||
if (_vCam == null) return;
|
if (_impulseSource == null) return; // 조건이 맞으면 종료할거에요 -> 임펄스 소스가 없으면 불가
|
||||||
if (_zoomCoroutine != null) StopCoroutine(_zoomCoroutine);
|
_impulseSource.GenerateImpulse(Vector3.right * 0.4f); // 임펄스를 발생시킬거에요 -> 오른쪽 방향으로 약하게 흔들기
|
||||||
float targetFOV = isZooming ? zoomedFOV : defaultFOV;
|
} // 코드 블록을 끝낼거에요 -> ShakeNoNo를
|
||||||
_zoomCoroutine = StartCoroutine(AnimateZoom(targetFOV));
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerator AnimateZoom(float target)
|
public void SetZoom(bool isZooming) // 함수를 선언할거에요 -> 줌인/줌아웃 목표를 설정하는 SetZoom을
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> SetZoom 범위를
|
||||||
if (_vCam == null) yield break;
|
if (_vCam == null) return; // 조건이 맞으면 종료할거에요 -> 카메라가 없으면 불가
|
||||||
while (Mathf.Abs(_vCam.m_Lens.FieldOfView - target) > 0.1f)
|
if (_zoomCoroutine != null) StopCoroutine(_zoomCoroutine); // 조건이 맞으면 실행할거에요 -> 기존 줌 코루틴이 있으면 중지해서 중첩 방지
|
||||||
{
|
float targetFOV = isZooming ? zoomedFOV : defaultFOV; // 목표 FOV를 정할거에요 -> 줌이면 zoomedFOV, 아니면 defaultFOV
|
||||||
_vCam.m_Lens.FieldOfView = Mathf.Lerp(_vCam.m_Lens.FieldOfView, target, Time.deltaTime * zoomSpeed);
|
_zoomCoroutine = StartCoroutine(AnimateZoom(targetFOV)); // 코루틴을 실행할거에요 -> 목표 FOV로 부드럽게 이동하는 AnimateZoom을
|
||||||
yield return null;
|
} // 코드 블록을 끝낼거에요 -> SetZoom을
|
||||||
}
|
|
||||||
_vCam.m_Lens.FieldOfView = target;
|
private IEnumerator AnimateZoom(float target) // 코루틴 함수를 선언할거에요 -> FOV를 목표값까지 보간하는 AnimateZoom을
|
||||||
}
|
{ // 코드 블록을 시작할거에요 -> AnimateZoom 범위를
|
||||||
}
|
if (_vCam == null) yield break; // 조건이 맞으면 중단할거에요 -> 카메라가 없으면 코루틴 종료
|
||||||
|
while (Mathf.Abs(_vCam.m_Lens.FieldOfView - target) > 0.1f) // 반복할거에요 -> 현재 FOV가 목표값과 충분히 가까워질 때까지
|
||||||
|
{ // 코드 블록을 시작할거에요 -> 보간 루프
|
||||||
|
_vCam.m_Lens.FieldOfView = Mathf.Lerp(_vCam.m_Lens.FieldOfView, target, Time.deltaTime * zoomSpeed); // 값을 보간할거에요 -> 현재 FOV를 target 쪽으로 부드럽게
|
||||||
|
yield return null; // 다음 프레임까지 기다릴거에요 -> 프레임 단위로 갱신
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 보간 루프
|
||||||
|
_vCam.m_Lens.FieldOfView = target; // 값을 확정할거에요 -> 마지막에 정확히 target으로 맞추기
|
||||||
|
} // 코드 블록을 끝낼거에요 -> AnimateZoom을
|
||||||
|
|
||||||
|
} // 코드 블록을 끝낼거에요 -> CinemachineShake를
|
||||||
|
|
@ -1,35 +1,35 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
public class PlayerAim : MonoBehaviour
|
public class PlayerAim : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerAim을
|
||||||
{
|
{
|
||||||
[SerializeField] private PlayerHealth health;
|
[SerializeField] private PlayerHealth health; // 변수를 선언할거에요 -> 플레이어 체력 스크립트를
|
||||||
|
|
||||||
[Header("회전 설정")]
|
[Header("회전 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 회전 설정 을
|
||||||
[SerializeField] private float rotationSpeed = 15f; // 감도 조절
|
[SerializeField] private float rotationSpeed = 15f; // 변수를 선언할거에요 -> 회전 속도(감도)를
|
||||||
[SerializeField] private float rotationDeadzone = 1.2f; // 근접전 '뒤돌기' 방지 범위
|
[SerializeField] private float rotationDeadzone = 1.2f; // 변수를 선언할거에요 -> 회전 무시 범위(데드존)를
|
||||||
|
|
||||||
public void RotateTowardsMouse()
|
public void RotateTowardsMouse() // 함수를 선언할거에요 -> 마우스 방향으로 회전하는 RotateTowardsMouse를
|
||||||
{
|
{
|
||||||
if (health != null && health.IsDead) return;
|
if (health != null && health.IsDead) return; // 조건이 맞으면 중단할거에요 -> 플레이어가 죽었다면
|
||||||
|
|
||||||
// 1. 캐릭터의 월드 위치를 화면 상의 2D 좌표로 변환합니다.
|
// 1. 캐릭터의 월드 위치를 화면 상의 2D 좌표로 변환합니다.
|
||||||
Vector3 playerScreenPos = Camera.main.WorldToScreenPoint(transform.position);
|
Vector3 playerScreenPos = Camera.main.WorldToScreenPoint(transform.position); // 변환할거에요 -> 캐릭터 월드 좌표를 화면 좌표로
|
||||||
|
|
||||||
// 2. 마우스의 화면 좌표를 가져옵니다.
|
// 2. 마우스의 화면 좌표를 가져옵니다.
|
||||||
Vector3 mousePos = Input.mousePosition;
|
Vector3 mousePos = Input.mousePosition; // 가져올거에요 -> 현재 마우스 위치를
|
||||||
|
|
||||||
// 3. 화면상에서 플레이어 -> 마우스로 향하는 2D 방향 벡터를 구합니다.
|
// 3. 화면상에서 플레이어 -> 마우스로 향하는 2D 방향 벡터를 구합니다.
|
||||||
Vector3 dir = mousePos - playerScreenPos;
|
Vector3 dir = mousePos - playerScreenPos; // 계산할거에요 -> 마우스와 플레이어 사이의 방향 벡터를
|
||||||
|
|
||||||
// 4. 거리가 너무 가까우면 회전을 무시합니다 (데드존 적용).
|
// 4. 거리가 너무 가까우면 회전을 무시합니다 (데드존 적용).
|
||||||
if (dir.magnitude < rotationDeadzone * 50f) return;
|
if (dir.magnitude < rotationDeadzone * 50f) return; // 조건이 맞으면 중단할거에요 -> 마우스가 캐릭터에 너무 가깝다면
|
||||||
|
|
||||||
// 5. 2D 방향 벡터를 각도(Atan2)로 변환합니다.
|
// 5. 2D 방향 벡터를 각도(Atan2)로 변환합니다.
|
||||||
// 화면의 X는 월드의 X, 화면의 Y는 월드의 Z에 대응합니다 (쿼터뷰/탑다운 기준).
|
// 화면의 X는 월드의 X, 화면의 Y는 월드의 Z에 대응합니다 (쿼터뷰/탑다운 기준).
|
||||||
float angle = Mathf.Atan2(dir.x, dir.y) * Mathf.Rad2Deg;
|
float angle = Mathf.Atan2(dir.x, dir.y) * Mathf.Rad2Deg; // 계산할거에요 -> 벡터를 각도로 변환해서
|
||||||
|
|
||||||
// 6. 계산된 각도로 캐릭터를 부드럽게 회전시킵니다.
|
// 6. 계산된 각도로 캐릭터를 부드럽게 회전시킵니다.
|
||||||
Quaternion targetRotation = Quaternion.Euler(0, angle, 0);
|
Quaternion targetRotation = Quaternion.Euler(0, angle, 0); // 생성할거에요 -> 목표 회전값(쿼터니언)을
|
||||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
|
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime); // 회전시킬거에요 -> 현재 회전에서 목표 회전으로 부드럽게
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,58 +1,60 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // Random.Range 같은 유니티 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
|
||||||
|
|
||||||
public class RandomStatCardInstance
|
public class RandomStatCardInstance // 클래스를 선언할거에요 -> 랜덤 스탯 카드 1장의 동작/상태를 담당하는 RandomStatCardInstance를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> RandomStatCardInstance 범위를
|
||||||
private RandomStatCardData data;
|
|
||||||
private StatType statType;
|
|
||||||
private int value;
|
|
||||||
private bool isConfirmed = false;
|
|
||||||
private Stats stats;
|
|
||||||
|
|
||||||
public RandomStatCardInstance(RandomStatCardData data, Stats stats)
|
private RandomStatCardData data; // 변수를 선언할거에요 -> 카드 데이터(가능 스탯/범위)를 담을 data를
|
||||||
{
|
private StatType statType; // 변수를 선언할거에요 -> 이번에 뽑힌 스탯 종류를 담을 statType을
|
||||||
this.data = data;
|
private int value; // 변수를 선언할거에요 -> 이번에 뽑힌 수치를 담을 value를
|
||||||
this.stats = stats;
|
private bool isConfirmed = false; // 변수를 선언할거에요 -> 이미 적용 확정했는지 여부를 isConfirmed에(기본 false)
|
||||||
}
|
private Stats stats; // 변수를 선언할거에요 -> 실제 스탯을 적용할 대상(Stats)을 stats에
|
||||||
|
|
||||||
public void RollPreview()
|
public RandomStatCardInstance(RandomStatCardData data, Stats stats) // 생성자를 선언할거에요 -> 데이터와 적용 대상을 받아 초기화하는 생성자를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> 생성자 범위를
|
||||||
statType = data.possibleStats[Random.Range(0, data.possibleStats.Length)];
|
this.data = data; // 값을 넣을거에요 -> 전달받은 data를 멤버 data에 저장
|
||||||
value = Random.Range(data.minValue, data.maxValue + 1);
|
this.stats = stats; // 값을 넣을거에요 -> 전달받은 stats를 멤버 stats에 저장
|
||||||
isConfirmed = false;
|
} // 코드 블록을 끝낼거에요 -> 생성자를
|
||||||
}
|
|
||||||
|
|
||||||
public string GetText()
|
public void RollPreview() // 함수를 선언할거에요 -> 카드 미리보기(랜덤 결과) 뽑는 RollPreview를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> RollPreview 범위를
|
||||||
string sign = value >= 0 ? "+" : "";
|
statType = data.possibleStats[Random.Range(0, data.possibleStats.Length)]; // 값을 뽑을거에요 -> 가능한 스탯 중 하나를 랜덤으로
|
||||||
return $"{statType} {sign}{value}";
|
value = Random.Range(data.minValue, data.maxValue + 1); // 값을 뽑을거에요 -> min~max 범위에서 랜덤 수치를(+1은 int 상한 포함)
|
||||||
}
|
isConfirmed = false; // 상태를 바꿀거에요 -> 아직 확정 전(false)로
|
||||||
|
} // 코드 블록을 끝낼거에요 -> RollPreview를
|
||||||
|
|
||||||
public void Confirm()
|
public string GetText() // 함수를 선언할거에요 -> UI에 보여줄 텍스트를 만드는 GetText를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> GetText 범위를
|
||||||
if (isConfirmed) return;
|
string sign = value >= 0 ? "+" : ""; // 값을 정할거에요 -> 양수면 + 표시, 음수면 빈 문자열로
|
||||||
ApplyStat();
|
return $"{statType} {sign}{value}"; // 문자열을 반환할거에요 -> "스탯 +값" 형태로
|
||||||
isConfirmed = true;
|
} // 코드 블록을 끝낼거에요 -> GetText를
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyStat()
|
public void Confirm() // 함수를 선언할거에요 -> 선택 확정 시 실제 적용하는 Confirm을
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> Confirm 범위를
|
||||||
if (stats == null) return;
|
if (isConfirmed) return; // 조건이 맞으면 종료할거에요 -> 이미 확정했으면 중복 적용 방지
|
||||||
|
ApplyStat(); // 함수를 실행할거에요 -> 뽑힌 스탯을 실제로 적용
|
||||||
|
isConfirmed = true; // 상태를 바꿀거에요 -> 이제 확정 완료(true)로
|
||||||
|
} // 코드 블록을 끝낼거에요 -> Confirm을
|
||||||
|
|
||||||
switch (statType)
|
private void ApplyStat() // 함수를 선언할거에요 -> 스탯 적용 로직을 처리하는 ApplyStat을
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> ApplyStat 범위를
|
||||||
case StatType.Health:
|
if (stats == null) return; // 조건이 맞으면 종료할거에요 -> 적용 대상이 없으면 아무것도 안 함
|
||||||
stats.AddMaxHealth(value);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case StatType.Speed:
|
switch (statType) // 분기할거에요 -> 어떤 스탯인지에 따라
|
||||||
stats.AddMoveSpeed(value);
|
{ // 코드 블록을 시작할거에요 -> switch 범위를
|
||||||
break;
|
case StatType.Health: // 조건을 처리할거에요 -> 체력 스탯이면
|
||||||
|
stats.AddMaxHealth(value); // 함수를 실행할거에요 -> 최대 체력에 value만큼 더하기
|
||||||
|
break; // 분기를 끝낼거에요 -> switch 탈출
|
||||||
|
|
||||||
// ✨ [제거] case StatType.Strength 삭제됨
|
case StatType.Speed: // 조건을 처리할거에요 -> 이동속도 스탯이면
|
||||||
|
stats.AddMoveSpeed(value); // 함수를 실행할거에요 -> 이동속도에 value만큼 더하기
|
||||||
|
break; // 분기를 끝낼거에요 -> switch 탈출
|
||||||
|
|
||||||
case StatType.Damage:
|
// ✨ [제거] case StatType.Strength 삭제됨 // 설명을 적을거에요 -> 힘 스탯은 더 이상 처리하지 않음
|
||||||
stats.AddAttackDamage(value);
|
|
||||||
break;
|
case StatType.Damage: // 조건을 처리할거에요 -> 공격력 스탯이면
|
||||||
}
|
stats.AddAttackDamage(value); // 함수를 실행할거에요 -> 공격 데미지에 value만큼 더하기
|
||||||
}
|
break; // 분기를 끝낼거에요 -> switch 탈출
|
||||||
}
|
} // 코드 블록을 끝낼거에요 -> switch를
|
||||||
|
} // 코드 블록을 끝낼거에요 -> ApplyStat을
|
||||||
|
|
||||||
|
} // 코드 블록을 끝낼거에요 -> RandomStatCardInstance를
|
||||||
|
|
@ -1,138 +1,135 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using System;
|
using UnityEngine.Events; // 유니티 이벤트 기능을 사용할거에요 -> UnityEngine.Events를
|
||||||
using System.Collections;
|
using System; // C# 기본 이벤트(Action)를 사용할거에요 -> System을
|
||||||
|
using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
|
||||||
|
|
||||||
public class PlayerHealth : MonoBehaviour, IDamageable
|
/// <summary>
|
||||||
|
/// 플레이어의 체력, 피격, 무적, 사망을 관리합니다.
|
||||||
|
/// </summary>
|
||||||
|
public class PlayerHealth : MonoBehaviour, IDamageable // 클래스를 선언할거에요 -> PlayerHealth를
|
||||||
{
|
{
|
||||||
[Header("=== 참조 ===")]
|
[Header("=== 참조 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 참조 === 를
|
||||||
[SerializeField] private Stats stats;
|
[SerializeField] private Stats playerStats; // 변수를 선언할거에요 -> 스탯 스크립트를 playerStats에
|
||||||
[SerializeField] private Animator animator;
|
|
||||||
[SerializeField] private PlayerAttack attackScript;
|
|
||||||
|
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
[Header("=== 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 설정 === 을
|
||||||
// 🔗 보스 패턴 스크립트 연결 (Inspector에서 할당)
|
[SerializeField] private float invincibleDuration = 1.0f; // 변수를 선언할거에요 -> 무적 시간을 invincibleDuration에
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
[SerializeField] private string hitAnimationName = "Player_Hit"; // 변수를 선언할거에요 -> 피격 애니메이션 이름을 hitAnimationName에
|
||||||
[Header("🔗 보스 패턴 연결")]
|
|
||||||
public BossPatternPhases bossPattern;
|
|
||||||
|
|
||||||
public bool IsDead { get; private set; }
|
[Header("=== 유니티 이벤트 (인스펙터 연결용) ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 유니티 이벤트 === 를
|
||||||
public bool isHit { get; private set; }
|
public UnityEvent OnDieEvent; // 이벤트를 선언할거에요 -> 사망 시 발생할 UnityEvent인 OnDieEvent를
|
||||||
public bool isInvincible; // 대시 중 무적 플래그
|
public UnityEvent<float> OnHealthChanged; // 이벤트를 선언할거에요 -> 체력 변경 시 발생할 OnHealthChanged를
|
||||||
|
|
||||||
public event Action OnHitEvent, OnDead;
|
// ⭐ [이벤트] += 연산자 오류 방지를 위해 event Action 사용
|
||||||
public event Action<float, float> OnHealthChanged;
|
public event Action OnHitEvent;
|
||||||
|
public event Action OnHit;
|
||||||
|
public event Action OnDie;
|
||||||
|
public event Action OnDead;
|
||||||
|
|
||||||
private float _currentHealth;
|
// ⭐ [핵심 수정 1] 외부에서 수정 가능하도록 set 권한 개방
|
||||||
|
public float CurrentHP { get; private set; } // (체력은 함부로 바꾸면 안되니 private set 유지)
|
||||||
|
|
||||||
private IEnumerator Start()
|
// "set 접근자에 액세스할 수 없습니다" 오류 해결 -> private 제거
|
||||||
|
public bool isInvincible { get; set; } = false;
|
||||||
|
|
||||||
|
public bool IsDead { get; private set; } = false; // 사망 여부는 내부에서만 관리
|
||||||
|
|
||||||
|
private Animator animator; // 변수를 선언할거에요 -> 애니메이터 컴포넌트를 담을 animator를
|
||||||
|
|
||||||
|
// ⭐ [핵심 수정 2] "보호 수준 때문에 액세스 불가" 오류 해결 -> public으로 변경
|
||||||
|
public bool isHit = false;
|
||||||
|
|
||||||
|
private void Awake() // 함수를 실행할거에요 -> 초기화 Awake를
|
||||||
{
|
{
|
||||||
yield return null;
|
animator = GetComponentInChildren<Animator>(); // 컴포넌트를 가져올거에요 -> 자식의 애니메이터를
|
||||||
|
if (playerStats == null) playerStats = GetComponent<Stats>(); // 조건이 맞으면 가져올거에요 -> 스탯 컴포넌트가 비어있다면 내 몸에서
|
||||||
if (stats != null)
|
|
||||||
{
|
|
||||||
_currentHealth = stats.MaxHealth;
|
|
||||||
OnHealthChanged?.Invoke(_currentHealth, stats.MaxHealth);
|
|
||||||
Debug.Log($"<color=cyan>[UI Sync]</color> 초기 체력 설정 완료: {_currentHealth}/{stats.MaxHealth}");
|
|
||||||
}
|
|
||||||
if (animator == null) animator = GetComponent<Animator>();
|
|
||||||
if (attackScript == null) attackScript = GetComponent<PlayerAttack>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RefreshHealthUI()
|
private void Start() // 함수를 실행할거에요 -> 시작 Start를
|
||||||
{
|
{
|
||||||
if (stats != null)
|
if (playerStats != null) // 조건이 맞으면 실행할거에요 -> 스탯이 있다면
|
||||||
{
|
{
|
||||||
_currentHealth = Mathf.Min(_currentHealth, stats.MaxHealth);
|
CurrentHP = playerStats.MaxHealth; // 값을 초기화할거에요 -> 현재 체력을 최대 체력으로
|
||||||
OnHealthChanged?.Invoke(_currentHealth, stats.MaxHealth);
|
}
|
||||||
|
else // 조건이 틀리면 실행할거에요 -> 스탯이 없다면
|
||||||
|
{
|
||||||
|
CurrentHP = 100f; // 값을 넣을거에요 -> 기본 체력 100으로
|
||||||
|
Debug.LogWarning("⚠️ Stats 컴포넌트가 없습니다! 기본 체력 100 적용."); // 경고를 출력할거에요
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshHealthUI(); // 함수를 실행할거에요 -> 초기 체력 UI 갱신을
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TakeDamage(float damage) // 함수를 선언할거에요 -> 외부에서 호출할 피격 함수 TakeDamage를
|
||||||
|
{
|
||||||
|
if (IsDead || isInvincible) return; // 조건이 맞으면 중단할거에요 -> 죽었거나 무적이라면
|
||||||
|
|
||||||
|
// 실제 데미지 적용
|
||||||
|
CurrentHP -= damage; // 값을 뺄거에요 -> 체력에서 데미지를
|
||||||
|
if (CurrentHP < 0) CurrentHP = 0; // 조건이 맞으면 보정할거에요 -> 체력이 음수가 안 되게 0으로
|
||||||
|
|
||||||
|
Debug.Log($"💥 피격! 데미지: {damage}, 남은 체력: {CurrentHP}"); // 로그를 출력할거에요 -> 피격 정보를
|
||||||
|
|
||||||
|
RefreshHealthUI(); // 함수를 실행할거에요 -> 체력바 갱신을
|
||||||
|
|
||||||
|
if (CurrentHP <= 0) // 조건이 맞으면 실행할거에요 -> 체력이 바닥났다면
|
||||||
|
{
|
||||||
|
Die(); // 함수를 실행할거에요 -> 사망 처리 Die를
|
||||||
|
}
|
||||||
|
else // 조건이 틀리면 실행할거에요 -> 아직 살았다면
|
||||||
|
{
|
||||||
|
StartCoroutine(HitRoutine()); // 코루틴을 실행할거에요 -> 피격 무적/경직 처리를
|
||||||
|
|
||||||
|
// 모든 이벤트 호출
|
||||||
|
OnHit?.Invoke();
|
||||||
|
OnHitEvent?.Invoke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
private void Die() // 함수를 선언할거에요 -> 사망 처리 Die를
|
||||||
// ⭐ 데미지를 받는 함수
|
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
public void TakeDamage(float amount)
|
|
||||||
{
|
{
|
||||||
// 무적이거나 이미 죽었으면 데미지 무시
|
if (IsDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면
|
||||||
if (isInvincible || IsDead) return;
|
|
||||||
|
|
||||||
_currentHealth = Mathf.Max(0, _currentHealth - amount);
|
IsDead = true; // 상태를 바꿀거에요 -> 사망 상태를 참으로
|
||||||
OnHealthChanged?.Invoke(_currentHealth, stats.MaxHealth);
|
|
||||||
|
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
OnDieEvent?.Invoke(); // 유니티 이벤트
|
||||||
// 🔗 NEW: 보스 패턴 시스템에 "피격당함"을 알림
|
OnDie?.Invoke(); // C# 이벤트
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
OnDead?.Invoke(); // C# 이벤트 (호환용)
|
||||||
if (bossPattern != null)
|
|
||||||
{
|
Debug.Log("💀 플레이어 사망!"); // 로그를 출력할거에요 -> 사망 메시지를
|
||||||
// BossPatternPhases 스크립트의 OnPlayerHit() 함수 호출
|
|
||||||
// 이 함수 안에서 "패턴 진행 중인지" 체크하고 XP 감점 처리
|
|
||||||
bossPattern.OnPlayerHit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⭐ 피격 시 트리거 및 상태 리셋 함수 호출
|
public void RefreshHealthUI() // 함수를 선언할거에요 -> UI를 갱신하는 RefreshHealthUI를
|
||||||
OnHit();
|
{
|
||||||
|
float ratio = (playerStats != null && playerStats.MaxHealth > 0) ? CurrentHP / playerStats.MaxHealth : 0; // 비율을 계산할거에요 -> 현재 체력 / 최대 체력으로
|
||||||
OnHitEvent?.Invoke(); // 이벤트 발생
|
OnHealthChanged?.Invoke(ratio); // 이벤트를 실행할거에요 -> UI 슬라이더 값을 업데이트하도록
|
||||||
|
|
||||||
if (!IsDead) StartHit();
|
|
||||||
if (_currentHealth <= 0) Die();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
private IEnumerator HitRoutine() // 코루틴 함수를 선언할거에요 -> 피격 경직 및 무적 처리 HitRoutine을
|
||||||
// ⭐ 피격 시 공격 상태 리셋
|
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
public void OnHit()
|
|
||||||
{
|
{
|
||||||
|
isHit = true; // 상태를 바꿀거에요 -> 경직 상태를 참으로
|
||||||
|
isInvincible = true; // 상태를 바꿀거에요 -> 무적 상태를 참으로
|
||||||
|
|
||||||
if (animator != null)
|
if (animator != null)
|
||||||
{
|
{
|
||||||
// 1. 남아있는 공격 트리거를 강제로 꺼버림 (유령 공격 방지)
|
animator.Play(hitAnimationName, 0, 0f); // 재생할거에요 -> 설정된 피격 애니메이션을
|
||||||
animator.ResetTrigger("Attack");
|
|
||||||
|
|
||||||
// 2. 다른 공격(예: 투척) 트리거가 있다면 그것도 리셋
|
|
||||||
animator.ResetTrigger("Throw");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. PlayerAttack 스크립트의 공격 중인 상태 플래그도 강제로 꺼줌
|
yield return new WaitForSeconds(0.3f); // 기다릴거에요 -> 경직 시간(0.3초)만큼
|
||||||
if (attackScript != null)
|
isHit = false; // 상태를 바꿀거에요 -> 경직 해제
|
||||||
|
|
||||||
|
yield return new WaitForSeconds(invincibleDuration - 0.3f); // 기다릴거에요 -> 남은 무적 시간만큼
|
||||||
|
isInvincible = false; // 상태를 바꿀거에요 -> 무적 해제
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Heal(float amount) // 함수를 선언할거에요 -> 체력을 회복하는 Heal을
|
||||||
{
|
{
|
||||||
attackScript.IsAttacking = false;
|
if (IsDead) return; // 조건이 맞으면 중단할거에요 -> 죽은 상태라면
|
||||||
}
|
|
||||||
|
|
||||||
Debug.Log("<color=yellow>[Combat]</color> 피격으로 인해 공격 예약 및 상태가 초기화되었습니다.");
|
CurrentHP += amount; // 값을 더할거에요 -> 체력에 회복량을
|
||||||
}
|
if (playerStats != null && CurrentHP > playerStats.MaxHealth) // 조건이 맞으면 실행할거에요 -> 최대 체력을 넘었다면
|
||||||
|
CurrentHP = playerStats.MaxHealth; // 값을 제한할거에요 -> 최대 체력으로
|
||||||
|
|
||||||
private void StartHit()
|
RefreshHealthUI(); // 함수를 실행할거에요 -> UI 갱신을
|
||||||
{
|
Debug.Log($"💚 체력 회복: {amount}, 현재: {CurrentHP}"); // 로그를 출력할거에요 -> 회복 정보를
|
||||||
isHit = true;
|
|
||||||
// 인스펙터에 적힌 피격 애니메이션 이름(HitAnime) 재생
|
|
||||||
if (animator != null) animator.Play("HitAnime", 0, 0f);
|
|
||||||
|
|
||||||
CancelInvoke(nameof(OnHitEnd));
|
|
||||||
Invoke(nameof(OnHitEnd), 0.25f);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnHitEnd() { isHit = false; }
|
|
||||||
|
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
// ⭐ 사망 처리
|
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
private void Die()
|
|
||||||
{
|
|
||||||
IsDead = true;
|
|
||||||
Cursor.visible = true;
|
|
||||||
Cursor.lockState = CursorLockMode.None;
|
|
||||||
OnDead?.Invoke();
|
|
||||||
|
|
||||||
// ★ ★ ★ NEW: 사망 시 임시 XP를 영구 XP로 전환 ★ ★ ★
|
|
||||||
if (ObsessionSystem.instance != null)
|
|
||||||
{
|
|
||||||
ObsessionSystem.instance.OnDeathConvertXP();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Heal(float amount)
|
|
||||||
{
|
|
||||||
if (IsDead) return;
|
|
||||||
_currentHealth = Mathf.Min(_currentHealth + amount, stats.MaxHealth);
|
|
||||||
OnHealthChanged?.Invoke(_currentHealth, stats.MaxHealth);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,14 +1,22 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
|
||||||
public class DamageBot : MonoBehaviour
|
|
||||||
{
|
public class DamageBot : MonoBehaviour // 클래스를 선언할거에요 -> 트리거 안에 있는 대상에게 주기적으로 데미지를 주는 DamageBot을
|
||||||
[SerializeField] private float damageAmount = 10f, damageInterval = 1.0f;
|
{ // 코드 블록을 시작할거에요 -> DamageBot 범위를
|
||||||
private float timer;
|
|
||||||
private void OnTriggerStay(Collider other)
|
[SerializeField] private float damageAmount = 10f, damageInterval = 1.0f; // 변수를 선언할거에요 -> 데미지량(10)과 데미지 간격(1초)을
|
||||||
{
|
private float timer; // 변수를 선언할거에요 -> 시간 누적용 타이머를 timer에
|
||||||
if (other.TryGetComponent<IDamageable>(out var target))
|
|
||||||
{
|
private void OnTriggerStay(Collider other) // 함수를 선언할거에요 -> 트리거 안에 머무는 동안 계속 호출되는 OnTriggerStay를
|
||||||
timer += Time.deltaTime;
|
{ // 코드 블록을 시작할거에요 -> OnTriggerStay 범위를
|
||||||
if (timer >= damageInterval) { target.TakeDamage(damageAmount); timer = 0f; }
|
if (other.TryGetComponent<IDamageable>(out var target)) // 조건을 검사할거에요 -> 대상이 IDamageable을 가지고 있는지
|
||||||
}
|
{ // 코드 블록을 시작할거에요 -> 데미지 가능 대상 처리
|
||||||
}
|
timer += Time.deltaTime; // 값을 더할거에요 -> 지난 프레임 시간만큼 타이머 누적
|
||||||
}
|
if (timer >= damageInterval) // 조건을 검사할거에요 -> 누적 시간이 간격을 넘었는지
|
||||||
|
{ // 코드 블록을 시작할거에요 -> 데미지 적용 처리
|
||||||
|
target.TakeDamage(damageAmount); // 함수를 실행할거에요 -> 대상에게 damageAmount만큼 데미지 주기
|
||||||
|
timer = 0f; // 값을 초기화할거에요 -> 다음 틱을 위해 타이머를 0으로
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 데미지 적용 처리
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 데미지 가능 대상 처리
|
||||||
|
} // 코드 블록을 끝낼거에요 -> OnTriggerStay를
|
||||||
|
|
||||||
|
} // 코드 블록을 끝낼거에요 -> DamageBot을
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
public interface IDamageable
|
public interface IDamageable // 인터페이스를 선언할거에요 -> 데미지를 받을 수 있는 대상의 규칙(IDamageable)을
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> IDamageable 범위를
|
||||||
// 공격을 받았을 때 실행될 함수입니다.
|
|
||||||
// 데미지 양(damage)을 인자로 받습니다.
|
// 공격을 받았을 때 실행될 함수입니다. // 설명을 적을거에요 -> 데미지 받을 때 호출되는 함수임을
|
||||||
void TakeDamage(float damage);
|
// 데미지 양(damage)을 인자로 받습니다. // 설명을 적을거에요 -> 들어오는 데미지 값이 damage라는 걸
|
||||||
}
|
void TakeDamage(float damage); // 함수 원형을 정의할거에요 -> float damage를 받아 데미지 처리하는 TakeDamage를
|
||||||
|
|
||||||
|
} // 코드 블록을 끝낼거에요 -> IDamageable을
|
||||||
|
|
@ -1,41 +1,43 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic; // List를 쓰기 위해 불러올거에요 -> System.Collections.Generic을
|
||||||
|
|
||||||
public class WeaponHitBox : MonoBehaviour
|
public class WeaponHitBox : MonoBehaviour // 클래스를 선언할거에요 -> 무기 히트박스(트리거)로 타격을 처리하는 WeaponHitBox를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> WeaponHitBox 범위를
|
||||||
private float _damage;
|
|
||||||
private bool _isActive = false;
|
|
||||||
private List<IDamageable> _hitTargets = new List<IDamageable>();
|
|
||||||
|
|
||||||
public void EnableHitBox(float damage)
|
private float _damage; // 변수를 선언할거에요 -> 이번 공격에서 적용할 데미지를 _damage에
|
||||||
{
|
private bool _isActive = false; // 변수를 선언할거에요 -> 히트박스 활성 여부를 _isActive에(기본 false)
|
||||||
_damage = damage;
|
private List<IDamageable> _hitTargets = new List<IDamageable>(); // 리스트를 만들거에요 -> 이미 때린 대상 저장용 _hitTargets를
|
||||||
_isActive = true;
|
|
||||||
_hitTargets.Clear();
|
|
||||||
gameObject.SetActive(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DisableHitBox()
|
public void EnableHitBox(float damage) // 함수를 선언할거에요 -> 히트박스를 켜고 데미지를 세팅하는 EnableHitBox를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> EnableHitBox 범위를
|
||||||
_isActive = false;
|
_damage = damage; // 값을 넣을거에요 -> 받은 damage를 _damage로 설정
|
||||||
gameObject.SetActive(false);
|
_isActive = true; // 상태를 바꿀거에요 -> 히트박스를 활성 상태(true)로
|
||||||
}
|
_hitTargets.Clear(); // 리스트를 비울거에요 -> 이전 공격에서 맞춘 기록을 초기화
|
||||||
|
gameObject.SetActive(true); // 오브젝트를 켤거에요 -> 히트박스 트리거 오브젝트 활성화
|
||||||
|
} // 코드 블록을 끝낼거에요 -> EnableHitBox를
|
||||||
|
|
||||||
private void OnTriggerEnter(Collider other)
|
public void DisableHitBox() // 함수를 선언할거에요 -> 히트박스를 끄는 DisableHitBox를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> DisableHitBox 범위를
|
||||||
if (!_isActive || other.CompareTag("Player")) return;
|
_isActive = false; // 상태를 바꿀거에요 -> 히트박스를 비활성(false)로
|
||||||
|
gameObject.SetActive(false); // 오브젝트를 끌거에요 -> 히트박스 트리거 오브젝트 비활성화
|
||||||
|
} // 코드 블록을 끝낼거에요 -> DisableHitBox를
|
||||||
|
|
||||||
// ⭐ [핵심] 몬스터의 감지 영역(Trigger)은 무시하고 '진짜 몸통'만 타격!
|
private void OnTriggerEnter(Collider other) // 함수를 선언할거에요 -> 트리거에 뭔가 들어오면 호출되는 OnTriggerEnter를
|
||||||
if (other.isTrigger) return;
|
{ // 코드 블록을 시작할거에요 -> OnTriggerEnter 범위를
|
||||||
|
if (!_isActive || other.CompareTag("Player")) return; // 조건이 맞으면 종료할거에요 -> 비활성이거나 플레이어면 무시
|
||||||
|
|
||||||
if (other.TryGetComponent<IDamageable>(out var target))
|
// ⭐ [핵심] 몬스터의 감지 영역(Trigger)은 무시하고 '진짜 몸통'만 타격! // 설명을 적을거에요 -> 트리거 콜라이더는 공격 대상으로 안 잡겠다는 뜻
|
||||||
{
|
if (other.isTrigger) return; // 조건이 맞으면 종료할거에요 -> 상대 콜라이더가 트리거면 무시
|
||||||
if (!_hitTargets.Contains(target))
|
|
||||||
{
|
if (other.TryGetComponent<IDamageable>(out var target)) // 조건을 검사할거에요 -> IDamageable을 가진 대상인지
|
||||||
target.TakeDamage(_damage);
|
{ // 코드 블록을 시작할거에요 -> 데미지 처리
|
||||||
_hitTargets.Add(target);
|
if (!_hitTargets.Contains(target)) // 조건을 검사할거에요 -> 이번 공격에서 아직 안 때린 대상인지
|
||||||
Debug.Log($"<color=green>[Hit]</color> {other.name}의 몸통을 정확히 타격!");
|
{ // 코드 블록을 시작할거에요 -> 1회 타격 처리
|
||||||
}
|
target.TakeDamage(_damage); // 함수를 실행할거에요 -> 대상에게 _damage만큼 데미지 주기
|
||||||
}
|
_hitTargets.Add(target); // 리스트에 추가할거에요 -> 중복 타격 방지를 위해 대상 기록
|
||||||
}
|
Debug.Log($"<color=green>[Hit]</color> {other.name}의 몸통을 정확히 타격!"); // 로그를 찍을거에요 -> 타격 확인
|
||||||
}
|
} // 코드 블록을 끝낼거에요 -> 1회 타격 처리
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 데미지 처리
|
||||||
|
} // 코드 블록을 끝낼거에요 -> OnTriggerEnter를
|
||||||
|
|
||||||
|
} // 코드 블록을 끝낼거에요 -> WeaponHitBox를
|
||||||
|
|
@ -1,39 +1,41 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 기본 기능 + ScriptableObject를 쓰기 위해 불러올거에요 -> UnityEngine를
|
||||||
|
|
||||||
[CreateAssetMenu(fileName = "NewWeapon", menuName = "Weapons/Config")]
|
[CreateAssetMenu(fileName = "NewWeapon", menuName = "Weapons/Config")] // 에셋 생성 메뉴를 추가할거에요 -> Create > Weapons > Config로 WeaponConfig를 만들 수 있게
|
||||||
public class WeaponConfig : ScriptableObject
|
public class WeaponConfig : ScriptableObject // 클래스를 선언할거에요 -> 무기 설정 데이터를 담는 ScriptableObject인 WeaponConfig를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> WeaponConfig 범위를
|
||||||
[Header("기본 능력치")]
|
|
||||||
[SerializeField] private float baseDamage = 10f;
|
|
||||||
[SerializeField] private int requiredStrength = 5;
|
|
||||||
|
|
||||||
public float BaseDamage => baseDamage;
|
[Header("기본 능력치")] // 인스펙터에 제목을 표시할거에요 -> 기본 능력치 섹션을
|
||||||
public int RequiredStrength => requiredStrength;
|
[SerializeField] private float baseDamage = 10f; // 변수를 선언할거에요 -> 기본 데미지를 10으로 baseDamage에
|
||||||
|
[SerializeField] private int requiredStrength = 5; // 변수를 선언할거에요 -> 요구 힘 스탯을 5로 requiredStrength에
|
||||||
|
|
||||||
[Header("애니메이션 설정")]
|
public float BaseDamage => baseDamage; // 프로퍼티를 선언할거에요 -> baseDamage를 읽기 전용으로 노출
|
||||||
[SerializeField] private string normalAttackTrigger = "Attack";
|
public int RequiredStrength => requiredStrength; // 프로퍼티를 선언할거에요 -> requiredStrength를 읽기 전용으로 노출
|
||||||
[SerializeField] private string chargingBool = "IsCharging";
|
|
||||||
[SerializeField] private string throwTrigger = "Throw";
|
|
||||||
|
|
||||||
public string NormalAttackTrigger => normalAttackTrigger;
|
[Header("애니메이션 설정")] // 인스펙터에 제목을 표시할거에요 -> 애니메이션 설정 섹션을
|
||||||
public string ChargingBool => chargingBool;
|
[SerializeField] private string normalAttackTrigger = "Attack"; // 변수를 선언할거에요 -> 일반 공격 트리거 이름을 normalAttackTrigger에
|
||||||
public string ThrowTrigger => throwTrigger;
|
[SerializeField] private string chargingBool = "IsCharging"; // 변수를 선언할거에요 -> 차징 여부 bool 파라미터 이름을 chargingBool에
|
||||||
|
[SerializeField] private string throwTrigger = "Throw"; // 변수를 선언할거에요 -> 던지기 트리거 이름을 throwTrigger에
|
||||||
|
|
||||||
[Header("차징 단계별 설정 (인스펙터 수정)")]
|
public string NormalAttackTrigger => normalAttackTrigger; // 프로퍼티를 선언할거에요 -> 일반 공격 트리거 문자열을 읽기 전용으로 노출
|
||||||
[SerializeField] private float forceLv1 = 10f;
|
public string ChargingBool => chargingBool; // 프로퍼티를 선언할거에요 -> 차징 bool 문자열을 읽기 전용으로 노출
|
||||||
[SerializeField] private float spreadLv1 = 25f;
|
public string ThrowTrigger => throwTrigger; // 프로퍼티를 선언할거에요 -> 던지기 트리거 문자열을 읽기 전용으로 노출
|
||||||
[SerializeField] private float forceLv2 = 18f;
|
|
||||||
[SerializeField] private float spreadLv2 = 8f;
|
|
||||||
[SerializeField] private float forceLv3 = 28f;
|
|
||||||
// Lv3 Spread는 인스펙터에서 보이지 않게 하거나, 로직에서 무시하도록 처리합니다.
|
|
||||||
|
|
||||||
// ⭐ 힘(Force)은 단계별로 가져옵니다.
|
[Header("차징 단계별 설정 (인스펙터 수정)")] // 인스펙터에 제목을 표시할거에요 -> 차징 단계별 설정 섹션을
|
||||||
public float GetForce(int lv) => lv == 3 ? forceLv3 : (lv == 2 ? forceLv2 : forceLv1);
|
[SerializeField] private float forceLv1 = 10f; // 변수를 선언할거에요 -> 1단계 힘(발사력)을 10으로 forceLv1에
|
||||||
|
[SerializeField] private float spreadLv1 = 25f; // 변수를 선언할거에요 -> 1단계 퍼짐(정확도)을 25로 spreadLv1에
|
||||||
|
[SerializeField] private float forceLv2 = 18f; // 변수를 선언할거에요 -> 2단계 힘(발사력)을 18로 forceLv2에
|
||||||
|
[SerializeField] private float spreadLv2 = 8f; // 변수를 선언할거에요 -> 2단계 퍼짐(정확도)을 8로 spreadLv2에
|
||||||
|
[SerializeField] private float forceLv3 = 28f; // 변수를 선언할거에요 -> 3단계 힘(발사력)을 28로 forceLv3에
|
||||||
|
// Lv3 Spread는 인스펙터에서 보이지 않게 하거나, 로직에서 무시하도록 처리합니다. // 설명을 적을거에요 -> 3단계는 퍼짐을 안 쓰는 설계임을
|
||||||
|
|
||||||
// ⭐ [수정] 정확도(Spread) 로직: 3단계는 무조건 0(직선) 반환!
|
// ⭐ 힘(Force)은 단계별로 가져옵니다. // 설명을 적을거에요 -> 단계에 맞는 force를 반환함을
|
||||||
public float GetSpread(int lv)
|
public float GetForce(int lv) => lv == 3 ? forceLv3 : (lv == 2 ? forceLv2 : forceLv1); // 값을 반환할거에요 -> lv에 따라 forceLv1/2/3 중 하나를
|
||||||
{
|
|
||||||
if (lv == 3) return 0f; // 풀차징은 무조건 일자로 날아감
|
// ⭐ [수정] 정확도(Spread) 로직: 3단계는 무조건 0(직선) 반환! // 설명을 적을거에요 -> 풀차징은 직선 고정임을
|
||||||
return (lv == 2) ? spreadLv2 : spreadLv1;
|
public float GetSpread(int lv) // 함수를 선언할거에요 -> 단계별 spread를 가져오는 GetSpread를
|
||||||
}
|
{ // 코드 블록을 시작할거에요 -> GetSpread 범위를
|
||||||
}
|
if (lv == 3) return 0f; // 조건이 맞으면 반환할거에요 -> 3단계면 퍼짐 0(직선)
|
||||||
|
return (lv == 2) ? spreadLv2 : spreadLv1; // 값을 반환할거에요 -> 2단계면 spreadLv2, 아니면 spreadLv1
|
||||||
|
} // 코드 블록을 끝낼거에요 -> GetSpread를
|
||||||
|
|
||||||
|
} // 코드 블록을 끝낼거에요 -> WeaponConfig를
|
||||||
|
|
@ -1,211 +1,211 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using UnityEngine.AI;
|
using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
|
||||||
using System.Collections;
|
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
||||||
using System.Collections.Generic; // ✅ 리스트 사용 필수
|
using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 돌진 공격 몬스터
|
/// 돌진 공격 몬스터
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ChargeMonster : MonsterClass
|
public class ChargeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 ChargeMonster를
|
||||||
{
|
{
|
||||||
[Header("=== 돌진 공격 설정 ===")]
|
[Header("=== 돌진 공격 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 돌진 공격 설정 === 을
|
||||||
[SerializeField] private float chargeSpeed = 15f;
|
[SerializeField] private float chargeSpeed = 15f; // 변수를 선언할거에요 -> 돌진 속도(15.0)를 chargeSpeed에
|
||||||
[SerializeField] private float chargeRange = 10f;
|
[SerializeField] private float chargeRange = 10f; // 변수를 선언할거에요 -> 돌진 시작 거리(10.0)를 chargeRange에
|
||||||
[SerializeField] private float chargeDuration = 2f;
|
[SerializeField] private float chargeDuration = 2f; // 변수를 선언할거에요 -> 돌진 지속 시간(2.0초)을 chargeDuration에
|
||||||
[SerializeField] private float chargeDelay = 3f;
|
[SerializeField] private float chargeDelay = 3f; // 변수를 선언할거에요 -> 돌진 쿨타임(3.0초)을 chargeDelay에
|
||||||
[SerializeField] private float prepareTime = 0.5f;
|
[SerializeField] private float prepareTime = 0.5f; // 변수를 선언할거에요 -> 돌진 준비 시간(0.5초)을 prepareTime에
|
||||||
|
|
||||||
// 🎁 [추가] 드랍 아이템 리스트
|
// 🎁 [추가] 드랍 아이템 리스트
|
||||||
[Header("=== 드랍 아이템 설정 ===")]
|
[Header("=== 드랍 아이템 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 드랍 아이템 설정 === 을
|
||||||
[Tooltip("드랍 가능한 아이템 리스트 (랜덤 1개)")]
|
[Tooltip("드랍 가능한 아이템 리스트 (랜덤 1개)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[SerializeField] private List<GameObject> dropItemPrefabs;
|
[SerializeField] private List<GameObject> dropItemPrefabs; // 리스트를 선언할거에요 -> 드랍 아이템 프리팹 목록을 dropItemPrefabs에
|
||||||
[Tooltip("아이템이 나올 확률 (0 ~ 100%)")]
|
[Tooltip("아이템이 나올 확률 (0 ~ 100%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[Range(0, 100)][SerializeField] private float dropChance = 30f;
|
[Range(0, 100)][SerializeField] private float dropChance = 30f; // 변수를 선언할거에요 -> 드랍 확률(30%)을 dropChance에
|
||||||
|
|
||||||
private float lastChargeTime;
|
private float lastChargeTime; // 변수를 선언할거에요 -> 마지막 돌진 시간을 lastChargeTime에
|
||||||
private bool isCharging = false;
|
private bool isCharging = false; // 변수를 선언할거에요 -> 돌진 중 여부를 isCharging에
|
||||||
private bool isPreparing = false;
|
private bool isPreparing = false; // 변수를 선언할거에요 -> 준비 중 여부를 isPreparing에
|
||||||
private Vector3 chargeDirection;
|
private Vector3 chargeDirection; // 변수를 선언할거에요 -> 돌진 방향 벡터를 chargeDirection에
|
||||||
|
|
||||||
[Header("공격 / 이동 애니메이션")]
|
[Header("공격 / 이동 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 공격 / 이동 애니메이션 을
|
||||||
[SerializeField] private string chargeAnimation = "Monster_Charge";
|
[SerializeField] private string chargeAnimation = "Monster_Charge"; // 변수를 선언할거에요 -> 돌진 애니메이션 이름을 chargeAnimation에
|
||||||
[SerializeField] private string prepareAnimation = "Monster_ChargePrepare";
|
[SerializeField] private string prepareAnimation = "Monster_ChargePrepare"; // 변수를 선언할거에요 -> 준비 애니메이션 이름을 prepareAnimation에
|
||||||
[SerializeField] private string Monster_Walk = "Monster_Walk";
|
[SerializeField] private string Monster_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 Monster_Walk에
|
||||||
|
|
||||||
[Header("AI 상세 설정")]
|
[Header("AI 상세 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 상세 설정 을
|
||||||
[SerializeField] private float patrolRadius = 5f;
|
[SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에
|
||||||
[SerializeField] private float patrolInterval = 2f;
|
[SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 간격(2.0초)을 patrolInterval에
|
||||||
|
|
||||||
private float nextPatrolTime;
|
private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에
|
||||||
private bool isPlayerInZone;
|
private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어 감지 여부를 isPlayerInZone에
|
||||||
private Rigidbody _rigidbody;
|
private Rigidbody _rigidbody; // 변수를 선언할거에요 -> 물리 컴포넌트를 담을 _rigidbody를
|
||||||
|
|
||||||
protected override void Awake()
|
protected override void Awake() // 함수를 덮어씌워 실행할거에요 -> Awake 초기화 로직을
|
||||||
{
|
{
|
||||||
base.Awake();
|
base.Awake(); // 부모의 함수를 실행할거에요 -> MonsterClass의 Awake를
|
||||||
_rigidbody = GetComponent<Rigidbody>();
|
_rigidbody = GetComponent<Rigidbody>(); // 컴포넌트를 가져올거에요 -> Rigidbody를
|
||||||
if (_rigidbody == null) _rigidbody = gameObject.AddComponent<Rigidbody>();
|
if (_rigidbody == null) _rigidbody = gameObject.AddComponent<Rigidbody>(); // 조건이 맞으면 실행할거에요 -> 없으면 새로 추가해서 _rigidbody에
|
||||||
_rigidbody.isKinematic = true;
|
_rigidbody.isKinematic = true; // 설정을 바꿀거에요 -> 물리 연산을 꺼두기(Kinematic)로
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Init()
|
protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기 설정을 Init에서
|
||||||
{
|
{
|
||||||
if (agent != null) agent.stoppingDistance = 1f;
|
if (agent != null) agent.stoppingDistance = 1f; // 조건이 맞으면 설정할거에요 -> 에이전트 정지 거리를 1.0으로
|
||||||
if (animator != null) animator.applyRootMotion = false;
|
if (animator != null) animator.applyRootMotion = false; // 조건이 맞으면 설정할거에요 -> 애니메이션 이동을 끄기로
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ExecuteAILogic()
|
protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을
|
||||||
{
|
{
|
||||||
if (isHit || isCharging || isPreparing) return;
|
if (isHit || isCharging || isPreparing) return; // 조건이 맞으면 중단할거에요 -> 피격, 돌진, 준비 중이라면
|
||||||
|
|
||||||
float distance = Vector3.Distance(transform.position, playerTransform.position);
|
float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를
|
||||||
|
|
||||||
if (isPlayerInZone || distance <= chargeRange * 1.5f)
|
if (isPlayerInZone || distance <= chargeRange * 1.5f) // 조건이 맞으면 실행할거에요 -> 감지 구역 안이거나 거리가 가까우면
|
||||||
{
|
{
|
||||||
HandleChargeCombat(distance);
|
HandleChargeCombat(distance); // 함수를 실행할거에요 -> 전투 로직인 HandleChargeCombat을
|
||||||
}
|
}
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 멀리 있다면
|
||||||
{
|
{
|
||||||
Patrol();
|
Patrol(); // 함수를 실행할거에요 -> 순찰을 도는 Patrol을
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateMovementAnimation();
|
UpdateMovementAnimation(); // 함수를 실행할거에요 -> 이동 애니메이션을 갱신하는 UpdateMovementAnimation을
|
||||||
}
|
}
|
||||||
|
|
||||||
void HandleChargeCombat(float distance)
|
void HandleChargeCombat(float distance) // 함수를 선언할거에요 -> 거리 기반 전투 판단 로직 HandleChargeCombat을
|
||||||
{
|
{
|
||||||
if (distance <= chargeRange && Time.time >= lastChargeTime + chargeDelay)
|
if (distance <= chargeRange && Time.time >= lastChargeTime + chargeDelay) // 조건이 맞으면 실행할거에요 -> 사거리 안이고 쿨타임이 지났다면
|
||||||
{
|
{
|
||||||
StartCoroutine(PrepareCharge());
|
StartCoroutine(PrepareCharge()); // 코루틴을 실행할거에요 -> 돌진 준비인 PrepareCharge를
|
||||||
}
|
}
|
||||||
else if (!isCharging && !isPreparing)
|
else if (!isCharging && !isPreparing) // 조건이 맞으면 실행할거에요 -> 돌진이나 준비 중이 아니라면 (추격)
|
||||||
{
|
{
|
||||||
if (agent.isOnNavMesh)
|
if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면
|
||||||
{
|
{
|
||||||
if (agent.isStopped) agent.isStopped = false;
|
if (agent.isStopped) agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀라고
|
||||||
agent.SetDestination(playerTransform.position);
|
agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerator PrepareCharge()
|
IEnumerator PrepareCharge() // 코루틴 함수를 선언할거에요 -> 돌진 준비 과정인 PrepareCharge를
|
||||||
{
|
{
|
||||||
isPreparing = true;
|
isPreparing = true; // 상태를 바꿀거에요 -> 준비 중 상태를 참(true)으로
|
||||||
if (agent.isOnNavMesh)
|
if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면
|
||||||
{
|
{
|
||||||
agent.isStopped = true;
|
agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고
|
||||||
agent.ResetPath();
|
agent.ResetPath(); // 명령을 내릴거에요 -> 경로를 초기화하라고
|
||||||
agent.velocity = Vector3.zero;
|
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
|
||||||
}
|
}
|
||||||
|
|
||||||
chargeDirection = (playerTransform.position - transform.position).normalized;
|
chargeDirection = (playerTransform.position - transform.position).normalized; // 벡터를 계산할거에요 -> 플레이어를 향하는 방향을
|
||||||
chargeDirection.y = 0;
|
chargeDirection.y = 0; // 값을 바꿀거에요 -> 높이 차이를 무시하게 y를 0으로
|
||||||
if (chargeDirection != Vector3.zero) transform.rotation = Quaternion.LookRotation(chargeDirection);
|
if (chargeDirection != Vector3.zero) transform.rotation = Quaternion.LookRotation(chargeDirection); // 회전시킬거에요 -> 돌진 방향을 바라보게
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(prepareAnimation)) animator.Play(prepareAnimation, 0, 0f);
|
if (!string.IsNullOrEmpty(prepareAnimation)) animator.Play(prepareAnimation, 0, 0f); // 재생할거에요 -> 준비 애니메이션을
|
||||||
|
|
||||||
Debug.Log("[ChargeMonster] 돌진 준비...");
|
Debug.Log("[ChargeMonster] 돌진 준비..."); // 로그를 출력할거에요 -> 준비 메시지를
|
||||||
yield return new WaitForSeconds(prepareTime);
|
yield return new WaitForSeconds(prepareTime); // 기다릴거에요 -> 준비 시간만큼
|
||||||
|
|
||||||
StartCoroutine(Charge());
|
StartCoroutine(Charge()); // 코루틴을 실행할거에요 -> 실제 돌진인 Charge를
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerator Charge()
|
IEnumerator Charge() // 코루틴 함수를 선언할거에요 -> 실제 돌진 동작인 Charge를
|
||||||
{
|
{
|
||||||
isPreparing = false;
|
isPreparing = false; // 상태를 바꿀거에요 -> 준비 상태를 거짓(false)으로
|
||||||
isCharging = true;
|
isCharging = true; // 상태를 바꿀거에요 -> 돌진 중 상태를 참(true)으로
|
||||||
lastChargeTime = Time.time;
|
lastChargeTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 돌진 시간으로
|
||||||
|
|
||||||
if (agent != null) agent.enabled = false;
|
if (agent != null) agent.enabled = false; // 기능을 끌거에요 -> 길찾기 에이전트를 (물리 이동을 위해)
|
||||||
if (_rigidbody != null) _rigidbody.isKinematic = false;
|
if (_rigidbody != null) _rigidbody.isKinematic = false; // 기능을 켤거에요 -> 물리 연산을 (충돌 감지를 위해)
|
||||||
|
|
||||||
animator.Play(chargeAnimation, 0, 0f);
|
animator.Play(chargeAnimation, 0, 0f); // 재생할거에요 -> 돌진 애니메이션을
|
||||||
Debug.Log("[ChargeMonster] 돌진 시작!");
|
Debug.Log("[ChargeMonster] 돌진 시작!"); // 로그를 출력할거에요 -> 돌진 시작 메시지를
|
||||||
|
|
||||||
float chargeStartTime = Time.time;
|
float chargeStartTime = Time.time; // 값을 저장할거에요 -> 돌진 시작 시간을
|
||||||
|
|
||||||
while (Time.time < chargeStartTime + chargeDuration)
|
while (Time.time < chargeStartTime + chargeDuration) // 반복할거에요 -> 지속 시간이 끝날 때까지
|
||||||
{
|
{
|
||||||
if (_rigidbody != null)
|
if (_rigidbody != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
|
||||||
_rigidbody.velocity = chargeDirection * chargeSpeed;
|
_rigidbody.velocity = chargeDirection * chargeSpeed; // 값을 넣을거에요 -> 속도를 돌진 방향과 속도로
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 리지드바디가 없다면 (비상용)
|
||||||
transform.position += chargeDirection * chargeSpeed * Time.deltaTime;
|
transform.position += chargeDirection * chargeSpeed * Time.deltaTime; // 이동시킬거에요 -> 위치를 직접 수정해서
|
||||||
yield return null;
|
yield return null; // 대기할거에요 -> 다음 프레임까지
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_rigidbody != null)
|
if (_rigidbody != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
|
||||||
{
|
{
|
||||||
_rigidbody.velocity = Vector3.zero;
|
_rigidbody.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
|
||||||
_rigidbody.isKinematic = true;
|
_rigidbody.isKinematic = true; // 기능을 끌거에요 -> 물리 연산을 (다시 NavMesh 이동을 위해)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (agent != null)
|
if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
|
||||||
{
|
{
|
||||||
agent.enabled = true;
|
agent.enabled = true; // 기능을 켤거에요 -> 길찾기 에이전트를
|
||||||
agent.ResetPath();
|
agent.ResetPath(); // 초기화할거에요 -> 경로를
|
||||||
agent.velocity = Vector3.zero;
|
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 내부 속도를 0으로
|
||||||
}
|
}
|
||||||
|
|
||||||
isCharging = false;
|
isCharging = false; // 상태를 바꿀거에요 -> 돌진 중 상태를 거짓(false)으로
|
||||||
Debug.Log("[ChargeMonster] 돌진 종료, 쿨타임 시작");
|
Debug.Log("[ChargeMonster] 돌진 종료, 쿨타임 시작"); // 로그를 출력할거에요 -> 종료 메시지를
|
||||||
}
|
}
|
||||||
|
|
||||||
void UpdateMovementAnimation()
|
void UpdateMovementAnimation() // 함수를 선언할거에요 -> 애니메이션 갱신 함수 UpdateMovementAnimation을
|
||||||
{
|
{
|
||||||
if (isCharging || isPreparing || isHit || isResting) return;
|
if (isCharging || isPreparing || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면
|
||||||
if (agent.enabled && agent.velocity.magnitude > 0.1f)
|
if (agent.enabled && agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 움직이고 있다면
|
||||||
animator.Play(Monster_Walk);
|
animator.Play(Monster_Walk); // 재생할거에요 -> 걷기 애니메이션을
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 멈춰 있다면
|
||||||
animator.Play(Monster_Idle);
|
animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션을
|
||||||
}
|
}
|
||||||
|
|
||||||
void Patrol()
|
void Patrol() // 함수를 선언할거에요 -> 순찰 로직 Patrol을
|
||||||
{
|
{
|
||||||
if (Time.time < nextPatrolTime) return;
|
if (Time.time < nextPatrolTime) return; // 조건이 맞으면 중단할거에요 -> 대기 시간이라면
|
||||||
Vector3 randomPoint = transform.position + new Vector3(
|
Vector3 randomPoint = transform.position + new Vector3( // 벡터를 계산할거에요 -> 랜덤 순찰 지점을
|
||||||
Random.Range(-patrolRadius, patrolRadius),
|
Random.Range(-patrolRadius, patrolRadius),
|
||||||
0,
|
0,
|
||||||
Random.Range(-patrolRadius, patrolRadius)
|
Random.Range(-patrolRadius, patrolRadius)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas))
|
if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> 유효한 NavMesh 위치라면
|
||||||
if (agent.isOnNavMesh) agent.SetDestination(hit.position);
|
if (agent.isOnNavMesh) agent.SetDestination(hit.position); // 명령을 내릴거에요 -> 그곳으로 이동하라고
|
||||||
|
|
||||||
nextPatrolTime = Time.time + patrolInterval;
|
nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCollisionEnter(Collision collision)
|
private void OnCollisionEnter(Collision collision) // 함수를 실행할거에요 -> 물리 충돌이 발생했을 때 OnCollisionEnter를
|
||||||
{
|
{
|
||||||
if (!isCharging) return;
|
if (!isCharging) return; // 조건이 맞으면 중단할거에요 -> 돌진 중이 아니라면
|
||||||
if (collision.gameObject.CompareTag("Player"))
|
if (collision.gameObject.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 충돌한 대상이 플레이어라면
|
||||||
{
|
{
|
||||||
if (collision.gameObject.TryGetComponent<PlayerHealth>(out var playerHealth))
|
if (collision.gameObject.TryGetComponent<PlayerHealth>(out var playerHealth)) // 컴포넌트를 가져올거에요 -> 플레이어 체력 스크립트를
|
||||||
{
|
{
|
||||||
if (!playerHealth.isInvincible)
|
if (!playerHealth.isInvincible) // 조건이 맞으면 실행할거에요 -> 무적 상태가 아니라면
|
||||||
playerHealth.TakeDamage(attackDamage);
|
playerHealth.TakeDamage(attackDamage); // 함수를 실행할거에요 -> 데미지를 입히는 TakeDamage를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ☠️ [추가] 죽을 때 아이템 드랍
|
// ☠️ [추가] 죽을 때 아이템 드랍
|
||||||
protected override void OnDie()
|
protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 사망 시 아이템 드랍 로직인 OnDie를
|
||||||
{
|
{
|
||||||
if (_rigidbody != null) _rigidbody.velocity = Vector3.zero;
|
if (_rigidbody != null) _rigidbody.velocity = Vector3.zero; // 값을 넣을거에요 -> 죽을 때 미끄러지지 않게 속도를 0으로
|
||||||
|
|
||||||
if (dropItemPrefabs != null && dropItemPrefabs.Count > 0)
|
if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) // 조건이 맞으면 실행할거에요 -> 아이템 리스트가 있다면
|
||||||
{
|
{
|
||||||
float randomValue = Random.Range(0f, 100f);
|
float randomValue = Random.Range(0f, 100f); // 값을 뽑을거에요 -> 0~100 사이 랜덤값을
|
||||||
if (randomValue <= dropChance)
|
if (randomValue <= dropChance) // 조건이 맞으면 실행할거에요 -> 확률에 당첨되었다면
|
||||||
{
|
{
|
||||||
int randomIndex = Random.Range(0, dropItemPrefabs.Count);
|
int randomIndex = Random.Range(0, dropItemPrefabs.Count); // 값을 뽑을거에요 -> 랜덤 인덱스를
|
||||||
if (dropItemPrefabs[randomIndex] != null)
|
if (dropItemPrefabs[randomIndex] != null) // 조건이 맞으면 실행할거에요 -> 아이템이 유효하다면
|
||||||
{
|
{
|
||||||
Instantiate(dropItemPrefabs[randomIndex], transform.position + Vector3.up * 0.5f, Quaternion.identity);
|
Instantiate(dropItemPrefabs[randomIndex], transform.position + Vector3.up * 0.5f, Quaternion.identity); // 생성할거에요 -> 아이템을 몬스터 위치에
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; }
|
private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } // 상태를 바꿀거에요 -> 플레이어 감지 시 참(true)으로
|
||||||
private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; }
|
private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } // 상태를 바꿀거에요 -> 플레이어가 나가면 거짓(false)으로
|
||||||
}
|
}
|
||||||
|
|
@ -1,211 +1,211 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using UnityEngine.AI;
|
using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
|
||||||
using System.Collections;
|
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 자폭 몬스터 (Kamikaze)
|
/// 자폭 몬스터 (Kamikaze)
|
||||||
/// - 플레이어에게 전력 질주
|
/// - 플레이어에게 전력 질주
|
||||||
/// - 범위 내 진입 시 제자리에서 카운트다운 후 폭발
|
/// - 범위 내 진입 시 제자리에서 카운트다운 후 폭발
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ExplodeMonster : MonsterClass
|
public class ExplodeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 ExplodeMonster를
|
||||||
{
|
{
|
||||||
[Header("=== 자폭 설정 ===")]
|
[Header("=== 자폭 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 자폭 설정 === 을
|
||||||
[SerializeField] private float explodeRange = 4f; // 데미지 입히는 범위 (폭발 반경)
|
[SerializeField] private float explodeRange = 4f; // 변수를 선언할거에요 -> 폭발 데미지 반경(4.0)을 explodeRange에
|
||||||
[SerializeField] private float triggerRange = 2.5f; // 폭발을 시작하는 거리 (이 안에 들어오면 카운트다운)
|
[SerializeField] private float triggerRange = 2.5f; // 변수를 선언할거에요 -> 자폭 시퀀스 시작 거리(2.5)를 triggerRange에
|
||||||
[SerializeField] private float fuseTime = 1.5f; // 지연 시간 (도망갈 기회 줌)
|
[SerializeField] private float fuseTime = 1.5f; // 변수를 선언할거에요 -> 폭발 지연 시간(1.5초)을 fuseTime에
|
||||||
[SerializeField] private float explosionDamage = 50f; // 폭발 데미지
|
[SerializeField] private float explosionDamage = 50f; // 변수를 선언할거에요 -> 폭발 데미지(50.0)를 explosionDamage에
|
||||||
|
|
||||||
[Header("폭발 효과")]
|
[Header("폭발 효과")] // 인스펙터 창에 제목을 표시할거에요 -> 폭발 효과 를
|
||||||
[SerializeField] private GameObject explosionEffectPrefab; // 쾅! 이펙트 프리팹
|
[SerializeField] private GameObject explosionEffectPrefab; // 변수를 선언할거에요 -> 폭발 이펙트 프리팹을 explosionEffectPrefab에
|
||||||
[SerializeField] private ParticleSystem fuseEffect; // 몸에서 불꽃 튀는 이펙트 (선택)
|
[SerializeField] private ParticleSystem fuseEffect; // 변수를 선언할거에요 -> 도화선(준비) 이펙트를 fuseEffect에
|
||||||
[SerializeField] private AudioClip fuseSound; // 치익~ 소리
|
[SerializeField] private AudioClip fuseSound; // 변수를 선언할거에요 -> 도화선 소리를 fuseSound에
|
||||||
[SerializeField] private AudioClip explosionSound; // 쾅! 소리
|
[SerializeField] private AudioClip explosionSound; // 변수를 선언할거에요 -> 폭발 소리를 explosionSound에
|
||||||
|
|
||||||
[Header("애니메이션")]
|
[Header("애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 애니메이션 을
|
||||||
[SerializeField] private string runAnimation = "Monster_Run"; // 달리기
|
[SerializeField] private string runAnimation = "Monster_Run"; // 변수를 선언할거에요 -> 달리기 애니메이션 이름을 runAnimation에
|
||||||
[SerializeField] private string fuseAnimation = "Monster_Fuse"; // 부들부들(폭발 준비)
|
[SerializeField] private string fuseAnimation = "Monster_Fuse"; // 변수를 선언할거에요 -> 자폭 준비 애니메이션 이름을 fuseAnimation에
|
||||||
|
|
||||||
[Header("AI 설정")]
|
[Header("AI 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 설정 을
|
||||||
[SerializeField] private float chaseSpeed = 6f; // 이동 속도 (다른 애들보다 빨라야 무서움)
|
[SerializeField] private float chaseSpeed = 6f; // 변수를 선언할거에요 -> 추격 속도(6.0)를 chaseSpeed에
|
||||||
[SerializeField] private float patrolRadius = 5f;
|
[SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에
|
||||||
[SerializeField] private float patrolInterval = 2f;
|
[SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 간격(2.0초)을 patrolInterval에
|
||||||
|
|
||||||
private bool isExploding = false; // 폭발 진행 중?
|
private bool isExploding = false; // 변수를 초기화할거에요 -> 폭발 진행 중 여부를 거짓(false)으로
|
||||||
private bool hasExploded = false; // 이미 터졌나?
|
private bool hasExploded = false; // 변수를 초기화할거에요 -> 이미 터졌는지 여부를 거짓(false)으로
|
||||||
private float nextPatrolTime;
|
private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에
|
||||||
private bool isPlayerInZone;
|
private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어 감지 여부를 isPlayerInZone에
|
||||||
|
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
// 초기화
|
// 초기화
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
protected override void Init()
|
protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을
|
||||||
{
|
{
|
||||||
if (agent != null)
|
if (agent != null)
|
||||||
{
|
{
|
||||||
agent.speed = chaseSpeed; // 속도 설정
|
agent.speed = chaseSpeed; // 값을 설정할거에요 -> 이동 속도를 추격 속도로
|
||||||
agent.stoppingDistance = 0.5f; // 바짝 붙음
|
agent.stoppingDistance = 0.5f; // 값을 설정할거에요 -> 정지 거리를 아주 짧게(0.5)
|
||||||
}
|
}
|
||||||
if (animator != null) animator.applyRootMotion = false;
|
if (animator != null) animator.applyRootMotion = false; // 설정을 바꿀거에요 -> 애니메이션 이동을 끄기로
|
||||||
|
|
||||||
if (fuseEffect != null) fuseEffect.Stop();
|
if (fuseEffect != null) fuseEffect.Stop(); // 기능을 실행할거에요 -> 도화선 이펙트가 켜져있다면 끄기를
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnResetStats()
|
protected override void OnResetStats() // 함수를 덮어씌워 실행할거에요 -> 스탯 초기화 로직인 OnResetStats를
|
||||||
{
|
{
|
||||||
isExploding = false;
|
isExploding = false; // 상태를 바꿀거에요 -> 폭발 진행 상태를 거짓(false)으로
|
||||||
hasExploded = false;
|
hasExploded = false; // 상태를 바꿀거에요 -> 폭발 완료 상태를 거짓(false)으로
|
||||||
if (fuseEffect != null) fuseEffect.Stop();
|
if (fuseEffect != null) fuseEffect.Stop(); // 기능을 실행할거에요 -> 도화선 이펙트를 끄기를
|
||||||
}
|
}
|
||||||
|
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
// AI 로직
|
// AI 로직
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
protected override void ExecuteAILogic()
|
protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을
|
||||||
{
|
{
|
||||||
if (isHit || isExploding || hasExploded) return;
|
if (isHit || isExploding || hasExploded) return; // 조건이 맞으면 중단할거에요 -> 피격, 자폭 중, 이미 폭발 상태라면
|
||||||
|
|
||||||
float distance = Vector3.Distance(transform.position, playerTransform.position);
|
float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를
|
||||||
|
|
||||||
// 플레이어가 감지 범위(15m) 안에 있거나 트리거에 닿았으면 추격
|
// 플레이어가 감지 범위(15m) 안에 있거나 트리거에 닿았으면 추격
|
||||||
if (isPlayerInZone || distance <= 15f)
|
if (isPlayerInZone || distance <= 15f) // 조건이 맞으면 실행할거에요 -> 추격 조건이 만족되면
|
||||||
{
|
{
|
||||||
ChasePlayer(distance);
|
ChasePlayer(distance); // 함수를 실행할거에요 -> 추격 및 자폭 처리를 하는 ChasePlayer를
|
||||||
}
|
}
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 멀리 있다면
|
||||||
{
|
{
|
||||||
Patrol();
|
Patrol(); // 함수를 실행할거에요 -> 순찰을 도는 Patrol을
|
||||||
UpdateMovementAnimation();
|
UpdateMovementAnimation(); // 함수를 실행할거에요 -> 애니메이션을 갱신하는 UpdateMovementAnimation을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChasePlayer(float distance)
|
void ChasePlayer(float distance) // 함수를 선언할거에요 -> 거리별 추격 행동을 정하는 ChasePlayer를
|
||||||
{
|
{
|
||||||
// 1. 폭발 시작 거리 안으로 들어왔다? -> 점화!
|
// 1. 폭발 시작 거리 안으로 들어왔다? -> 점화!
|
||||||
if (distance <= triggerRange)
|
if (distance <= triggerRange) // 조건이 맞으면 실행할거에요 -> 자폭 거리 이내라면
|
||||||
{
|
{
|
||||||
StartCoroutine(ExplodeRoutine());
|
StartCoroutine(ExplodeRoutine()); // 코루틴을 실행할거에요 -> 자폭 시퀀스인 ExplodeRoutine을
|
||||||
return;
|
return; // 중단할거에요 -> 더 이상 이동하지 않도록
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 아직 멀었다? -> 전력 질주
|
// 2. 아직 멀었다? -> 전력 질주
|
||||||
if (agent.isOnNavMesh)
|
if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면
|
||||||
{
|
{
|
||||||
agent.isStopped = false;
|
agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀라고
|
||||||
agent.SetDestination(playerTransform.position);
|
agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로
|
||||||
|
|
||||||
// 달리기 애니메이션
|
// 달리기 애니메이션
|
||||||
animator.Play(runAnimation);
|
animator.Play(runAnimation); // 재생할거에요 -> 달리기 애니메이션을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UpdateMovementAnimation()
|
void UpdateMovementAnimation() // 함수를 선언할거에요 -> 애니메이션 갱신 함수 UpdateMovementAnimation을
|
||||||
{
|
{
|
||||||
if (isExploding || isHit) return;
|
if (isExploding || isHit) return; // 조건이 맞으면 중단할거에요 -> 폭발 중이거나 맞고 있다면
|
||||||
|
|
||||||
if (agent.velocity.magnitude > 0.1f)
|
if (agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 움직이고 있다면
|
||||||
animator.Play(runAnimation);
|
animator.Play(runAnimation); // 재생할거에요 -> 달리기 애니메이션을
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 멈춰 있다면
|
||||||
animator.Play(Monster_Idle);
|
animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션을
|
||||||
}
|
}
|
||||||
|
|
||||||
void Patrol()
|
void Patrol() // 함수를 선언할거에요 -> 순찰 로직 Patrol을
|
||||||
{
|
{
|
||||||
if (Time.time < nextPatrolTime) return;
|
if (Time.time < nextPatrolTime) return; // 조건이 맞으면 중단할거에요 -> 대기 시간이라면
|
||||||
|
|
||||||
Vector3 randomPoint = transform.position + new Vector3(
|
Vector3 randomPoint = transform.position + new Vector3( // 벡터를 계산할거에요 -> 랜덤 순찰 지점을
|
||||||
Random.Range(-patrolRadius, patrolRadius),
|
Random.Range(-patrolRadius, patrolRadius),
|
||||||
0,
|
0,
|
||||||
Random.Range(-patrolRadius, patrolRadius)
|
Random.Range(-patrolRadius, patrolRadius)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas))
|
if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> 유효한 위치라면
|
||||||
if (agent.isOnNavMesh) agent.SetDestination(hit.position);
|
if (agent.isOnNavMesh) agent.SetDestination(hit.position); // 명령을 내릴거에요 -> 이동하라고
|
||||||
|
|
||||||
nextPatrolTime = Time.time + patrolInterval;
|
nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을
|
||||||
}
|
}
|
||||||
|
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
// 💣 폭발 시퀀스 (점화 -> 대기 -> 쾅!)
|
// 💣 폭발 시퀀스 (점화 -> 대기 -> 쾅!)
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
IEnumerator ExplodeRoutine()
|
IEnumerator ExplodeRoutine() // 코루틴 함수를 선언할거에요 -> 자폭 진행 과정인 ExplodeRoutine을
|
||||||
{
|
{
|
||||||
if (hasExploded) yield break;
|
if (hasExploded) yield break; // 조건이 맞으면 종료할거에요 -> 이미 터졌다면
|
||||||
|
|
||||||
isExploding = true;
|
isExploding = true; // 상태를 바꿀거에요 -> 폭발 진행 상태를 참(true)으로
|
||||||
hasExploded = true;
|
hasExploded = true; // 상태를 바꿀거에요 -> 폭발 완료 상태를 참(true)으로 (중복 방지)
|
||||||
isAttacking = true; // 부모 클래스 간섭 방지
|
isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로 (부모 간섭 방지)
|
||||||
|
|
||||||
// ⭐ [핵심] 급브레이크! (문워크 방지)
|
// ⭐ [핵심] 급브레이크! (문워크 방지)
|
||||||
if (agent.isOnNavMesh)
|
if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면
|
||||||
{
|
{
|
||||||
agent.isStopped = true;
|
agent.isStopped = true; // 명령을 내릴거에요 -> 멈추라고
|
||||||
agent.ResetPath(); // 목적지 삭제
|
agent.ResetPath(); // 명령을 내릴거에요 -> 경로를 지우라고
|
||||||
agent.velocity = Vector3.zero; // 속도 0
|
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.Log($"💣 자폭 카운트다운! ({fuseTime}초 뒤 폭발)");
|
Debug.Log($"💣 자폭 카운트다운! ({fuseTime}초 뒤 폭발)"); // 로그를 출력할거에요 -> 자폭 예고 메시지를
|
||||||
|
|
||||||
// 1. 부들부들 떨기 (폭발 준비 모션)
|
// 1. 부들부들 떨기 (폭발 준비 모션)
|
||||||
if (!string.IsNullOrEmpty(fuseAnimation)) animator.Play(fuseAnimation);
|
if (!string.IsNullOrEmpty(fuseAnimation)) animator.Play(fuseAnimation); // 재생할거에요 -> 준비 애니메이션을
|
||||||
|
|
||||||
// 2. 치익~ 소리 & 이펙트
|
// 2. 치익~ 소리 & 이펙트
|
||||||
if (fuseEffect != null) fuseEffect.Play();
|
if (fuseEffect != null) fuseEffect.Play(); // 실행할거에요 -> 도화선 이펙트를
|
||||||
if (fuseSound != null && audioSource != null) audioSource.PlayOneShot(fuseSound);
|
if (fuseSound != null && audioSource != null) audioSource.PlayOneShot(fuseSound); // 재생할거에요 -> 도화선 소리를
|
||||||
|
|
||||||
// 3. 도망갈 시간 주기
|
// 3. 도망갈 시간 주기
|
||||||
yield return new WaitForSeconds(fuseTime);
|
yield return new WaitForSeconds(fuseTime); // 기다릴거에요 -> 폭발 지연 시간만큼
|
||||||
|
|
||||||
// 4. 쾅!
|
// 4. 쾅!
|
||||||
PerformExplosion();
|
PerformExplosion(); // 함수를 실행할거에요 -> 실제 폭발 처리를
|
||||||
}
|
}
|
||||||
|
|
||||||
void PerformExplosion()
|
void PerformExplosion() // 함수를 선언할거에요 -> 폭발 데미지와 이펙트를 처리하는 PerformExplosion을
|
||||||
{
|
{
|
||||||
Debug.Log("💥💥💥 쾅!!!");
|
Debug.Log("💥💥💥 쾅!!!"); // 로그를 출력할거에요 -> 폭발 메시지를
|
||||||
|
|
||||||
// 폭발 이펙트 생성 (Particle System)
|
// 폭발 이펙트 생성 (Particle System)
|
||||||
if (explosionEffectPrefab != null)
|
if (explosionEffectPrefab != null) // 조건이 맞으면 실행할거에요 -> 폭발 프리팹이 있다면
|
||||||
{
|
{
|
||||||
Instantiate(explosionEffectPrefab, transform.position, Quaternion.identity);
|
Instantiate(explosionEffectPrefab, transform.position, Quaternion.identity); // 생성할거에요 -> 폭발 이펙트를 현재 위치에
|
||||||
}
|
}
|
||||||
|
|
||||||
// 폭발음
|
// 폭발음
|
||||||
if (explosionSound != null)
|
if (explosionSound != null) // 조건이 맞으면 실행할거에요 -> 폭발음이 있다면
|
||||||
{
|
{
|
||||||
AudioSource.PlayClipAtPoint(explosionSound, transform.position, 1f);
|
AudioSource.PlayClipAtPoint(explosionSound, transform.position, 1f); // 재생할거에요 -> 폭발 소리를 해당 위치에서
|
||||||
}
|
}
|
||||||
|
|
||||||
// 주변 데미지 처리
|
// 주변 데미지 처리
|
||||||
DamageNearbyTargets();
|
DamageNearbyTargets(); // 함수를 실행할거에요 -> 주변 대상에게 데미지를 주는 DamageNearbyTargets를
|
||||||
|
|
||||||
// 몬스터 사망 처리 (MonsterClass 기능 사용)
|
// 몬스터 사망 처리 (MonsterClass 기능 사용)
|
||||||
Die();
|
Die(); // 함수를 실행할거에요 -> 몬스터를 죽게 만드는 Die를
|
||||||
}
|
}
|
||||||
|
|
||||||
void DamageNearbyTargets()
|
void DamageNearbyTargets() // 함수를 선언할거에요 -> 폭발 범위 내 데미지를 입히는 DamageNearbyTargets를
|
||||||
{
|
{
|
||||||
// 폭발 범위(Sphere) 안에 있는 모든 물체 검사
|
// 폭발 범위(Sphere) 안에 있는 모든 물체 검사
|
||||||
Collider[] hitColliders = Physics.OverlapSphere(transform.position, explodeRange);
|
Collider[] hitColliders = Physics.OverlapSphere(transform.position, explodeRange); // 배열에 담을거에요 -> 폭발 범위 내의 충돌체들을
|
||||||
|
|
||||||
foreach (Collider hit in hitColliders)
|
foreach (Collider hit in hitColliders) // 반복할거에요 -> 감지된 모든 충돌체에 대해
|
||||||
{
|
{
|
||||||
// 플레이어 데미지
|
// 플레이어 데미지
|
||||||
if (hit.CompareTag("Player"))
|
if (hit.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 대상이 플레이어라면
|
||||||
{
|
{
|
||||||
if (hit.TryGetComponent<PlayerHealth>(out var playerHealth))
|
if (hit.TryGetComponent<PlayerHealth>(out var playerHealth)) // 컴포넌트를 가져올거에요 -> 플레이어 체력 스크립트를
|
||||||
{
|
{
|
||||||
if (!playerHealth.isInvincible)
|
if (!playerHealth.isInvincible) // 조건이 맞으면 실행할거에요 -> 무적 상태가 아니라면
|
||||||
{
|
{
|
||||||
playerHealth.TakeDamage(explosionDamage);
|
playerHealth.TakeDamage(explosionDamage); // 함수를 실행할거에요 -> 폭발 데미지를 입히는 TakeDamage를
|
||||||
Debug.Log($"💥 플레이어 폭사! 데미지: {explosionDamage}");
|
Debug.Log($"💥 플레이어 폭사! 데미지: {explosionDamage}"); // 로그를 출력할거에요 -> 피격 메시지를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// (선택사항) 주변 몬스터도 팀킬?
|
// (선택사항) 주변 몬스터도 팀킬?
|
||||||
else if (hit.CompareTag("Enemy") && hit.gameObject != gameObject)
|
else if (hit.CompareTag("Enemy") && hit.gameObject != gameObject) // 조건이 맞으면 실행할거에요 -> 대상이 다른 몬스터라면
|
||||||
{
|
{
|
||||||
// 팀킬 로직이 필요하면 여기에 추가
|
// 팀킬 로직이 필요하면 여기에 추가
|
||||||
}
|
}
|
||||||
|
|
@ -217,24 +217,24 @@ public class ExplodeMonster : MonsterClass
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
// 자폭병은 때려도 폭발 안 멈춤 (취향에 따라 수정 가능)
|
// 자폭병은 때려도 폭발 안 멈춤 (취향에 따라 수정 가능)
|
||||||
protected override void OnStartHit() { }
|
protected override void OnStartHit() { } // 함수를 비워둘거에요 -> 피격 시 아무 행동도 안 하게 (폭발 캔슬 방지)
|
||||||
|
|
||||||
// 죽을 때 퓨즈 끄기
|
// 죽을 때 퓨즈 끄기
|
||||||
protected override void OnDie()
|
protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 사망 시 정리를 위해
|
||||||
{
|
{
|
||||||
if (fuseEffect != null && fuseEffect.isPlaying) fuseEffect.Stop();
|
if (fuseEffect != null && fuseEffect.isPlaying) fuseEffect.Stop(); // 기능을 실행할거에요 -> 도화선 이펙트가 켜져있다면 끄기를
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; }
|
private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } // 상태를 바꿀거에요 -> 플레이어 진입 시 감지 상태를 참으로
|
||||||
private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; }
|
private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } // 상태를 바꿀거에요 -> 플레이어 이탈 시 감지 상태를 거짓으로
|
||||||
|
|
||||||
// 에디터에서 범위 보여주기
|
// 에디터에서 범위 보여주기
|
||||||
private void OnDrawGizmosSelected()
|
private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 에디터에서 범위를 그리는 OnDrawGizmosSelected를
|
||||||
{
|
{
|
||||||
Gizmos.color = Color.red;
|
Gizmos.color = Color.red; // 색상을 설정할거에요 -> 감지 범위 색을 빨간색으로
|
||||||
Gizmos.DrawWireSphere(transform.position, triggerRange); // 감지 범위
|
Gizmos.DrawWireSphere(transform.position, triggerRange); // 그림을 그릴거에요 -> 자폭 감지 범위를
|
||||||
|
|
||||||
Gizmos.color = new Color(1f, 0.5f, 0f, 0.3f);
|
Gizmos.color = new Color(1f, 0.5f, 0f, 0.3f); // 색상을 설정할거에요 -> 폭발 범위 색을 주황색 반투명으로
|
||||||
Gizmos.DrawSphere(transform.position, explodeRange); // 폭발 데미지 범위
|
Gizmos.DrawSphere(transform.position, explodeRange); // 그림을 그릴거에요 -> 폭발 데미지 범위를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,77 +1,80 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic; // 제네릭 컬렉션을 쓰기 위해 불러올거에요 -> System.Collections.Generic을
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
|
||||||
|
|
||||||
public class MonsterWeapon : MonoBehaviour
|
public class MonsterWeapon : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 MonsterWeapon을
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> MonsterWeapon 범위를
|
||||||
[Header("무기 설정")]
|
|
||||||
[Tooltip("이 무기 고유의 공격력 (예: 10)")]
|
|
||||||
[SerializeField] private float weaponBaseDamage = 10f;
|
|
||||||
|
|
||||||
private float _finalDamage;
|
[Header("무기 설정")] // 인스펙터에 제목을 표시할거에요 -> 무기 설정을
|
||||||
[SerializeField] private BoxCollider _weaponCollider;
|
[Tooltip("이 무기 고유의 공격력 (예: 10)")] // 인스펙터에 툴팁을 달거에요 -> weaponBaseDamage 설명을
|
||||||
|
[SerializeField] private float weaponBaseDamage = 10f; // 변수를 선언할거에요 -> 무기 기본 공격력을 10으로
|
||||||
|
|
||||||
private void Awake()
|
private float _finalDamage; // 변수를 선언할거에요 -> 최종 데미지를 저장할 _finalDamage를
|
||||||
{
|
[SerializeField] private BoxCollider _weaponCollider; // 변수를 선언할거에요 -> 무기 콜라이더를 저장할 _weaponCollider를
|
||||||
_weaponCollider = GetComponent<BoxCollider>();
|
|
||||||
_finalDamage = weaponBaseDamage;
|
|
||||||
//DisableHitBox();
|
|
||||||
EnableHitBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetDamage(float monsterStrength)
|
private void Awake() // 함수를 선언할거에요 -> 시작 시 1번 실행되는 Awake를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> Awake 범위를
|
||||||
_finalDamage = weaponBaseDamage + monsterStrength;
|
_weaponCollider = GetComponent<BoxCollider>(); // 컴포넌트를 가져올거에요 -> 내 오브젝트의 BoxCollider를 찾아 _weaponCollider에
|
||||||
Debug.Log($"✅ [무기] 데미지 설정됨: 총 {_finalDamage}");
|
_finalDamage = weaponBaseDamage; // 값을 넣을거에요 -> 최종 데미지를 기본 데미지로 초기화
|
||||||
}
|
//DisableHitBox(); // 주석 처리할거에요 -> 시작 시 판정을 끄는 코드(현재는 안 씀)
|
||||||
|
EnableHitBox(); // 함수를 실행할거에요 -> 시작하자마자 판정을 켜기(현재 설정)
|
||||||
|
} // 코드 블록을 끝낼거에요 -> Awake를
|
||||||
|
|
||||||
public void EnableHitBox()
|
public void SetDamage(float monsterStrength) // 함수를 선언할거에요 -> 몬스터 스탯을 받아 최종 데미지를 세팅하는 SetDamage를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> SetDamage 범위를
|
||||||
Debug.Log("enabletest");
|
_finalDamage = weaponBaseDamage + monsterStrength; // 값을 계산할거에요 -> 기본 데미지 + 몬스터 힘으로 최종 데미지 설정
|
||||||
|
Debug.Log($"✅ [무기] 데미지 설정됨: 총 {_finalDamage}"); // 로그를 찍을거에요 -> 최종 데미지가 얼마인지
|
||||||
|
} // 코드 블록을 끝낼거에요 -> SetDamage를
|
||||||
|
|
||||||
if (_weaponCollider != null)
|
public void EnableHitBox() // 함수를 선언할거에요 -> 공격 판정을 켜는 EnableHitBox를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> EnableHitBox 범위를
|
||||||
Debug.Log("setcollider");
|
Debug.Log("enabletest"); // 로그를 찍을거에요 -> 함수가 호출됐는지 확인용
|
||||||
_weaponCollider.enabled = true;
|
|
||||||
Debug.Log("🔴 [무기] 공격 판정 ON! (휘두르기 시작)"); // 로그 추가
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DisableHitBox()
|
if (_weaponCollider != null) // 조건을 검사할거에요 -> 콜라이더가 정상적으로 잡혔는지
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> 콜라이더가 있을 때 처리
|
||||||
if (_weaponCollider != null)
|
Debug.Log("setcollider"); // 로그를 찍을거에요 -> 콜라이더 enable 직전 확인용
|
||||||
{
|
_weaponCollider.enabled = true; // 값을 바꿀거에요 -> 트리거 판정을 켜기
|
||||||
_weaponCollider.enabled = false;
|
Debug.Log("🔴 [무기] 공격 판정 ON! (휘두르기 시작)"); // 로그를 찍을거에요 -> 판정이 켜졌음을
|
||||||
Debug.Log("⚪ [무기] 공격 판정 OFF (휘두르기 끝)"); // 로그 추가
|
} // 코드 블록을 끝낼거에요 -> 콜라이더 처리
|
||||||
}
|
} // 코드 블록을 끝낼거에요 -> EnableHitBox를
|
||||||
}
|
|
||||||
|
|
||||||
// ⭐ 여기가 핵심! 닿는 모든 것을 기록함
|
public void DisableHitBox() // 함수를 선언할거에요 -> 공격 판정을 끄는 DisableHitBox를
|
||||||
private void OnTriggerEnter(Collider other)
|
{ // 코드 블록을 시작할거에요 -> DisableHitBox 범위를
|
||||||
{
|
if (_weaponCollider != null) // 조건을 검사할거에요 -> 콜라이더가 있는지
|
||||||
// 1. 무엇이든 닿으면 일단 로그를 찍음 (범인 색출)
|
{ // 코드 블록을 시작할거에요 -> 콜라이더가 있을 때 처리
|
||||||
Debug.Log($"💥 [충돌 감지] 무기가 닿은 것: {other.name} / 태그: {other.tag}");
|
_weaponCollider.enabled = false; // 값을 바꿀거에요 -> 트리거 판정을 끄기
|
||||||
|
Debug.Log("⚪ [무기] 공격 판정 OFF (휘두르기 끝)"); // 로그를 찍을거에요 -> 판정이 꺼졌음을
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 콜라이더 처리
|
||||||
|
} // 코드 블록을 끝낼거에요 -> DisableHitBox를
|
||||||
|
|
||||||
if (other.CompareTag("Player"))
|
// ⭐ 여기가 핵심! 닿는 모든 것을 기록함 // 설명을 적을거에요 -> 트리거 충돌 감지 핵심 구간임을
|
||||||
{
|
private void OnTriggerEnter(Collider other) // 함수를 선언할거에요 -> 트리거에 다른 콜라이더가 들어오면 호출되는 OnTriggerEnter를
|
||||||
PlayerHealth playerHealth = other.GetComponent<PlayerHealth>();
|
{ // 코드 블록을 시작할거에요 -> OnTriggerEnter 범위를
|
||||||
|
// 1. 무엇이든 닿으면 일단 로그를 찍음 (범인 색출) // 설명을 적을거에요 -> 뭐랑 부딪히는지 디버깅용
|
||||||
|
Debug.Log($"💥 [충돌 감지] 무기가 닿은 것: {other.name} / 태그: {other.tag}"); // 로그를 찍을거에요 -> 충돌한 오브젝트 이름/태그를
|
||||||
|
|
||||||
if (playerHealth != null)
|
if (other.CompareTag("Player")) // 조건을 검사할거에요 -> 닿은 대상이 Player 태그인지
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> 플레이어일 때 처리
|
||||||
if (!playerHealth.isInvincible)
|
PlayerHealth playerHealth = other.GetComponent<PlayerHealth>(); // 컴포넌트를 가져올거에요 -> 플레이어의 PlayerHealth를 찾아 playerHealth에
|
||||||
{
|
|
||||||
playerHealth.TakeDamage(_finalDamage);
|
if (playerHealth != null) // 조건을 검사할거에요 -> PlayerHealth가 실제로 붙어있는지
|
||||||
Debug.Log($"⚔️ [적중] 플레이어 체력 깎임! (-{_finalDamage})");
|
{ // 코드 블록을 시작할거에요 -> PlayerHealth가 있을 때 처리
|
||||||
}
|
if (!playerHealth.isInvincible) // 조건을 검사할거에요 -> 플레이어가 무적인지 아닌지
|
||||||
else
|
{ // 코드 블록을 시작할거에요 -> 무적이 아닐 때 처리
|
||||||
{
|
playerHealth.TakeDamage(_finalDamage); // 함수를 실행할거에요 -> 플레이어에게 최종 데미지 적용
|
||||||
Debug.Log("🛡️ [방어] 플레이어가 무적 상태입니다.");
|
Debug.Log($"⚔️ [적중] 플레이어 체력 깎임! (-{_finalDamage})"); // 로그를 찍을거에요 -> 데미지가 들어갔음을
|
||||||
}
|
} // 코드 블록을 끝낼거에요 -> 무적 아닐 때 처리
|
||||||
DisableHitBox();
|
else // 조건이 아니면 실행할거에요 -> 무적이면
|
||||||
}
|
{ // 코드 블록을 시작할거에요 -> 무적일 때 처리
|
||||||
else
|
Debug.Log("🛡️ [방어] 플레이어가 무적 상태입니다."); // 로그를 찍을거에요 -> 무적이라 데미지 안 들어감을
|
||||||
{
|
} // 코드 블록을 끝낼거에요 -> 무적 처리
|
||||||
Debug.LogError("⚠️ [오류] 플레이어 태그는 있는데 'PlayerHealth' 스크립트가 없습니다!");
|
|
||||||
}
|
DisableHitBox(); // 함수를 실행할거에요 -> 한 번 맞추면 바로 판정 끄기(중복 타격 방지)
|
||||||
}
|
} // 코드 블록을 끝낼거에요 -> PlayerHealth 있을 때 처리
|
||||||
}
|
else // 조건이 아니면 실행할거에요 -> PlayerHealth가 없으면
|
||||||
}
|
{ // 코드 블록을 시작할거에요 -> 오류 처리
|
||||||
|
Debug.LogError("⚠️ [오류] 플레이어 태그는 있는데 'PlayerHealth' 스크립트가 없습니다!"); // 에러 로그를 찍을거에요 -> 컴포넌트 누락 경고
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 오류 처리
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 플레이어 처리
|
||||||
|
} // 코드 블록을 끝낼거에요 -> OnTriggerEnter를
|
||||||
|
|
||||||
|
} // 코드 블록을 끝낼거에요 -> MonsterWeapon을
|
||||||
|
|
@ -1,155 +1,163 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using UnityEngine.AI;
|
using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
|
||||||
using System.Collections;
|
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
||||||
using System;
|
using System; // 기본 시스템 기능(Action 등)을 사용할거에요 -> System을
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 몬스터 기본 클래스 (공통 기능만)
|
/// 몬스터 기본 클래스 (공통 기능만)
|
||||||
/// - 공격 방식은 자식 클래스에서 구현
|
/// - 공격 방식은 자식 클래스에서 구현
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class MonsterClass : MonoBehaviour, IDamageable
|
public abstract class MonsterClass : MonoBehaviour, IDamageable // 추상 클래스를 선언할거에요 -> MonoBehaviour와 IDamageable을 상속받는 MonsterClass를
|
||||||
{
|
{
|
||||||
[Header("--- 최적화 설정 ---")]
|
[Header("--- 최적화 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 최적화 설정 --- 을
|
||||||
protected Renderer mobRenderer;
|
protected Renderer mobRenderer; // 변수를 선언할거에요 -> 몬스터의 렌더러(외형) 컴포넌트를 담을 mobRenderer를
|
||||||
protected Transform playerTransform;
|
protected Transform playerTransform; // 변수를 선언할거에요 -> 플레이어의 위치 정보를 담을 playerTransform을
|
||||||
[SerializeField] protected float optimizationDistance = 40f;
|
[SerializeField] protected float optimizationDistance = 40f; // 변수를 선언할거에요 -> 몬스터 최적화(AI 정지) 거리(40.0)를 optimizationDistance에
|
||||||
|
|
||||||
[Header("몬스터 기본 스탯")]
|
[Header("몬스터 기본 스탯")] // 인스펙터 창에 제목을 표시할거에요 -> 몬스터 기본 스탯 을
|
||||||
[SerializeField] protected float maxHP = 100f;
|
[SerializeField] protected float maxHP = 100f; // 변수를 선언할거에요 -> 최대 체력(100.0)을 maxHP에
|
||||||
[SerializeField] protected float attackDamage = 10f;
|
[SerializeField] protected float attackDamage = 10f; // 변수를 선언할거에요 -> 공격력(10.0)을 attackDamage에
|
||||||
[SerializeField] protected int expReward = 10;
|
[SerializeField] protected int expReward = 10; // 변수를 선언할거에요 -> 처치 시 경험치 보상(10)을 expReward에
|
||||||
[SerializeField] protected float moveSpeed = 3.5f;
|
[SerializeField] protected float moveSpeed = 3.5f; // 변수를 선언할거에요 -> 이동 속도(3.5)를 moveSpeed에
|
||||||
|
|
||||||
protected float currentHP;
|
protected float currentHP; // 변수를 선언할거에요 -> 현재 체력을 저장할 currentHP를
|
||||||
public event Action<float, float> OnHealthChanged;
|
public event Action<float, float> OnHealthChanged; // 이벤트를 선언할거에요 -> 체력이 변할 때 알릴 OnHealthChanged를
|
||||||
|
|
||||||
[Header("전투 / 무기 (선택사항)")]
|
[Header("전투 / 무기 (선택사항)")] // 인스펙터 창에 제목을 표시할거에요 -> 전투 / 무기 (선택사항) 을
|
||||||
// ⭐ 근접 몬스터가 사용할 무기 (원거리 몬스터는 비워둬도 됨)
|
// ⭐ 근접 몬스터가 사용할 무기 (원거리 몬스터는 비워둬도 됨)
|
||||||
[SerializeField] protected MonsterWeapon myWeapon;
|
[SerializeField] protected MonsterWeapon myWeapon; // 변수를 선언할거에요 -> 몬스터가 장착한 무기 스크립트를 myWeapon에
|
||||||
|
|
||||||
[Header("피격 / 사망 / 대기 애니메이션")]
|
[Header("피격 / 사망 / 대기 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 피격 / 사망 / 대기 애니메이션 을
|
||||||
[SerializeField] protected string Monster_Idle = "Monster_Idle";
|
[SerializeField] protected string Monster_Idle = "Monster_Idle"; // 변수를 선언할거에요 -> 대기 애니메이션 이름("Monster_Idle")을 Monster_Idle에
|
||||||
[SerializeField] protected string Monster_GetDamage = "Monster_GetDamage";
|
[SerializeField] protected string Monster_GetDamage = "Monster_GetDamage"; // 변수를 선언할거에요 -> 피격 애니메이션 이름("Monster_GetDamage")을 Monster_GetDamage에
|
||||||
[SerializeField] protected string Monster_Die = "Monster_Die";
|
[SerializeField] protected string Monster_Die = "Monster_Die"; // 변수를 선언할거에요 -> 사망 애니메이션 이름("Monster_Die")을 Monster_Die에
|
||||||
|
|
||||||
protected Animator animator;
|
protected Animator animator; // 변수를 선언할거에요 -> 애니메이션 제어 컴포넌트를 담을 animator를
|
||||||
protected NavMeshAgent agent;
|
protected NavMeshAgent agent; // 변수를 선언할거에요 -> 길찾기 에이전트 컴포넌트를 담을 agent를
|
||||||
protected AudioSource audioSource;
|
protected AudioSource audioSource; // 변수를 선언할거에요 -> 소리 재생 컴포넌트를 담을 audioSource를
|
||||||
protected bool isHit, isDead, isAttacking;
|
|
||||||
|
|
||||||
public bool IsAggroed { get; protected set; }
|
// ⭐ [핵심] 자식 클래스에서 공유할 상태 변수들 (중복 선언 방지)
|
||||||
|
protected bool isHit;
|
||||||
|
protected bool isDead;
|
||||||
|
protected bool isAttacking;
|
||||||
|
|
||||||
[Header("AI 설정")]
|
public bool IsAggroed { get; protected set; } // 프로퍼티를 선언할거에요 -> 어그로(전투) 상태 여부를 외부에서 읽기만 가능하게 IsAggroed에
|
||||||
[SerializeField] protected float attackRestDuration = 1.5f;
|
|
||||||
protected bool isResting;
|
|
||||||
|
|
||||||
public static System.Action<int> OnMonsterKilled;
|
[Header("AI 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 설정 을
|
||||||
|
[SerializeField] protected float attackRestDuration = 1.5f; // 변수를 선언할거에요 -> 공격 후 대기 시간(1.5초)을 attackRestDuration에
|
||||||
|
protected bool isResting; // 변수를 선언할거에요 -> 휴식 중인지 여부를 저장할 isResting을
|
||||||
|
|
||||||
[Header("공통 사운드/이펙트")]
|
public static System.Action<int> OnMonsterKilled; // 정적 이벤트를 선언할거에요 -> 몬스터 처치 시 경험치를 전달할 OnMonsterKilled를
|
||||||
[SerializeField] protected AudioClip hitSound, deathSound;
|
|
||||||
[SerializeField] protected GameObject deathEffectPrefab;
|
[Header("공통 사운드/이펙트")] // 인스펙터 창에 제목을 표시할거에요 -> 공통 사운드/이펙트 를
|
||||||
[SerializeField] protected ParticleSystem hitEffect;
|
[SerializeField] protected AudioClip hitSound, deathSound; // 변수를 선언할거에요 -> 피격음과 사망음 오디오 클립을
|
||||||
[SerializeField] protected Transform impactSpawnPoint;
|
[SerializeField] protected GameObject deathEffectPrefab; // 변수를 선언할거에요 -> 사망 시 재생할 이펙트 프리팹을
|
||||||
|
[SerializeField] protected ParticleSystem hitEffect; // 변수를 선언할거에요 -> 피격 시 재생할 파티클 시스템을
|
||||||
|
[SerializeField] protected Transform impactSpawnPoint; // 변수를 선언할거에요 -> 이펙트가 생성될 위치를
|
||||||
|
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
// 생명주기
|
// 생명주기
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
protected virtual void Awake()
|
protected virtual void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 호출되는 Awake를
|
||||||
{
|
{
|
||||||
mobRenderer = GetComponentInChildren<Renderer>();
|
mobRenderer = GetComponentInChildren<Renderer>(); // 컴포넌트를 가져올거에요 -> 자식 오브젝트의 렌더러를 mobRenderer에
|
||||||
animator = GetComponent<Animator>();
|
animator = GetComponent<Animator>(); // 컴포넌트를 가져올거에요 -> 내 몸의 애니메이터를 animator에
|
||||||
agent = GetComponent<NavMeshAgent>();
|
agent = GetComponent<NavMeshAgent>(); // 컴포넌트를 가져올거에요 -> 내 몸의 길찾기 에이전트를 agent에
|
||||||
audioSource = GetComponent<AudioSource>();
|
audioSource = GetComponent<AudioSource>(); // 컴포넌트를 가져올거에요 -> 내 몸의 오디오 소스를 audioSource에
|
||||||
if (agent != null) agent.speed = moveSpeed;
|
if (agent != null) agent.speed = moveSpeed; // 조건이 맞으면 설정할거에요 -> 에이전트가 있다면 이동 속도를 moveSpeed로
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void OnEnable()
|
protected virtual void OnEnable() // 함수를 실행할거에요 -> 오브젝트가 활성화될 때 호출되는 OnEnable을
|
||||||
{
|
{
|
||||||
playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform;
|
playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform; // 오브젝트를 찾을거에요 -> "Player" 태그를 가진 오브젝트의 위치를 playerTransform에
|
||||||
if (mobRenderer != null) mobRenderer.enabled = true;
|
if (mobRenderer != null) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 있다면 보이게 켜기(true)를
|
||||||
Init();
|
Init(); // 함수를 실행할거에요 -> 자식 클래스에서 정의할 초기화 함수 Init을
|
||||||
if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.RegisterMob(this);
|
if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.RegisterMob(this); // 조건이 맞으면 실행할거에요 -> 매니저가 있다면 나(this)를 관리 목록에 등록하기를
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void OnDisable()
|
protected virtual void OnDisable() // 함수를 실행할거에요 -> 오브젝트가 비활성화될 때 호출되는 OnDisable을
|
||||||
{
|
{
|
||||||
if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.UnregisterMob(this);
|
if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.UnregisterMob(this); // 조건이 맞으면 실행할거에요 -> 매니저가 있다면 나(this)를 관리 목록에서 제외하기를
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ResetStats()
|
// ⭐ [수정] Update를 virtual로 선언하여 자식이 재정의 가능하게 함
|
||||||
|
protected virtual void Update()
|
||||||
{
|
{
|
||||||
isDead = false;
|
OnManagedUpdate(); // 기본적으로 매니저 업데이트 로직을 수행 (일반 몬스터용)
|
||||||
IsAggroed = false;
|
}
|
||||||
currentHP = maxHP;
|
|
||||||
OnHealthChanged?.Invoke(currentHP, maxHP);
|
|
||||||
|
|
||||||
Collider col = GetComponent<Collider>();
|
public void ResetStats() // 함수를 선언할거에요 -> 몬스터 상태를 초기화하는 ResetStats를
|
||||||
if (col != null) col.enabled = true;
|
{
|
||||||
|
isDead = false; // 상태를 바꿀거에요 -> 사망 상태를 거짓(false)으로
|
||||||
|
IsAggroed = false; // 상태를 바꿀거에요 -> 어그로 상태를 거짓(false)으로
|
||||||
|
currentHP = maxHP; // 값을 넣을거에요 -> 현재 체력을 최대 체력으로
|
||||||
|
OnHealthChanged?.Invoke(currentHP, maxHP); // 이벤트를 알릴거에요 -> 체력이 변경되었음을 UI 등에
|
||||||
|
|
||||||
if (agent != null) agent.speed = moveSpeed; // 스피드 초기화
|
Collider col = GetComponent<Collider>(); // 컴포넌트를 가져올거에요 -> 내 몸의 콜라이더를 col에
|
||||||
|
if (col != null) col.enabled = true; // 조건이 맞으면 실행할거에요 -> 콜라이더가 있다면 다시 켜기(true)를
|
||||||
|
|
||||||
OnResetStats();
|
if (agent != null) agent.speed = moveSpeed; // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 속도를 초기화하기를
|
||||||
|
|
||||||
|
OnResetStats(); // 함수를 실행할거에요 -> 자식 클래스의 추가 초기화 함수 OnResetStats를
|
||||||
}
|
}
|
||||||
|
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
// 추상 메서드 (자식이 반드시 구현)
|
// 추상 메서드 (자식이 반드시 구현)
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
protected virtual void Init() { }
|
protected virtual void Init() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 초기화 함수 Init을
|
||||||
protected abstract void ExecuteAILogic();
|
protected abstract void ExecuteAILogic(); // 추상 함수를 선언할거에요 -> 자식에서 반드시 구현해야 할 AI 로직 ExecuteAILogic을
|
||||||
protected virtual void OnResetStats() { }
|
protected virtual void OnResetStats() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 스탯 초기화 함수 OnResetStats를
|
||||||
|
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
// 공통 기능
|
// 공통 기능
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
public virtual void Reactivate()
|
public virtual void Reactivate() // 함수를 선언할거에요 -> 몬스터를 재활성화하는 Reactivate를
|
||||||
{
|
{
|
||||||
if (agent != null && agent.isOnNavMesh)
|
if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 있고 바닥에 있다면
|
||||||
{
|
{
|
||||||
agent.isStopped = false;
|
agent.isStopped = false; // 명령을 내릴거에요 -> 이동 멈춤을 풀라(false)고
|
||||||
if (IsAggroed && playerTransform != null)
|
if (IsAggroed && playerTransform != null) // 조건이 맞으면 실행할거에요 -> 어그로가 끌렸고 플레이어가 있다면
|
||||||
{
|
{
|
||||||
agent.SetDestination(playerTransform.position);
|
agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mobRenderer != null) mobRenderer.enabled = true;
|
if (mobRenderer != null) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 있다면 다시 켜기(true)를
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnManagedUpdate()
|
public virtual void OnManagedUpdate() // 함수를 선언할거에요 -> 매니저가 호출해줄 업데이트 함수 OnManagedUpdate를
|
||||||
{
|
{
|
||||||
if (isDead || playerTransform == null || !gameObject.activeInHierarchy) return;
|
if (isDead || playerTransform == null || !gameObject.activeInHierarchy) return; // 조건이 맞으면 중단할거에요 -> 죽었거나, 플레이어가 없거나, 꺼져 있다면
|
||||||
|
|
||||||
float distance = Vector3.Distance(transform.position, playerTransform.position);
|
float distance = Vector3.Distance(transform.position, playerTransform.position); // 값을 계산할거에요 -> 나와 플레이어 사이의 거리를 distance에
|
||||||
|
|
||||||
if (distance > optimizationDistance && !IsAggroed)
|
if (distance > optimizationDistance && !IsAggroed) // 조건이 맞으면 실행할거에요 -> 거리가 멀고 어그로 상태가 아니라면
|
||||||
{
|
{
|
||||||
StopMovement();
|
StopMovement(); // 함수를 실행할거에요 -> 움직임을 멈추는 StopMovement를
|
||||||
if (mobRenderer != null && mobRenderer.enabled) mobRenderer.enabled = false;
|
if (mobRenderer != null && mobRenderer.enabled) mobRenderer.enabled = false; // 조건이 맞으면 실행할거에요 -> 렌더러가 켜져 있다면 끄기(false)를 (최적화)
|
||||||
return;
|
return; // 중단할거에요 -> AI 로직을 실행하지 않도록 함수를
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mobRenderer != null && !mobRenderer.enabled) mobRenderer.enabled = true;
|
if (mobRenderer != null && !mobRenderer.enabled) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 꺼져 있다면 다시 켜기(true)를
|
||||||
if (mobRenderer != null && !mobRenderer.isVisible) { StopMovement(); return; }
|
if (mobRenderer != null && !mobRenderer.isVisible) { StopMovement(); return; } // 조건이 맞으면 실행할거에요 -> 렌더러가 화면에 보이지 않는다면 멈추고 중단하기를
|
||||||
if (agent != null && agent.isOnNavMesh && agent.isStopped) agent.isStopped = false;
|
if (agent != null && agent.isOnNavMesh && agent.isStopped) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 에이전트가 멈춰있다면 다시 움직이게(false) 하기를
|
||||||
|
|
||||||
ExecuteAILogic();
|
ExecuteAILogic(); // 함수를 실행할거에요 -> 실제 몬스터의 AI 로직을
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void StopMovement()
|
protected void StopMovement() // 함수를 선언할거에요 -> 이동을 멈추는 StopMovement를
|
||||||
{
|
{
|
||||||
if (agent != null && agent.isOnNavMesh)
|
if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 정상 작동 중이라면
|
||||||
{
|
{
|
||||||
agent.isStopped = true;
|
agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
|
||||||
agent.velocity = Vector3.zero;
|
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 이동 속도를 0으로
|
||||||
}
|
}
|
||||||
if (animator != null)
|
if (animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면
|
||||||
{
|
{
|
||||||
animator.SetFloat("Speed", 0f);
|
animator.SetFloat("Speed", 0f); // 값을 전달할거에요 -> 속도 파라미터를 0으로
|
||||||
animator.Play(Monster_Idle);
|
animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션(Monster_Idle)을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,210 +165,195 @@ public abstract class MonsterClass : MonoBehaviour, IDamageable
|
||||||
// 피격 / 사망
|
// 피격 / 사망
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
public virtual void TakeDamage(float amount)
|
public virtual void TakeDamage(float amount) // 함수를 선언할거에요 -> 외부에서 데미지를 줄 때 호출할 TakeDamage를
|
||||||
{
|
{
|
||||||
OnDamaged(amount);
|
OnDamaged(amount); // 함수를 실행할거에요 -> 실제 피격 처리를 담당하는 OnDamaged를
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void OnDamaged(float damage)
|
public virtual void OnDamaged(float damage) // 함수를 선언할거에요 -> 데미지 계산 로직인 OnDamaged를
|
||||||
{
|
{
|
||||||
if (isDead) return;
|
if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면
|
||||||
IsAggroed = true;
|
IsAggroed = true; // 상태를 바꿀거에요 -> 공격받았으니 어그로 상태를 참(true)으로
|
||||||
currentHP -= damage;
|
currentHP -= damage; // 값을 뺄거에요 -> 체력에서 데미지(damage)만큼을
|
||||||
OnHealthChanged?.Invoke(currentHP, maxHP);
|
OnHealthChanged?.Invoke(currentHP, maxHP); // 이벤트를 알릴거에요 -> 체력이 깎였음을
|
||||||
if (currentHP <= 0) { Die(); return; }
|
if (currentHP <= 0) { Die(); return; } // 조건이 맞으면 실행할거에요 -> 체력이 0 이하라면 사망 함수(Die)를
|
||||||
if (!isHit) StartHit();
|
if (!isHit) StartHit(); // 조건이 맞으면 실행할거에요 -> 피격 상태가 아니라면 피격 연출(StartHit)을
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void StartHit()
|
protected virtual void StartHit() // 함수를 선언할거에요 -> 피격 연출을 시작하는 StartHit을
|
||||||
{
|
{
|
||||||
isHit = true;
|
isHit = true; // 상태를 바꿀거에요 -> 피격 중 상태를 참(true)으로
|
||||||
isAttacking = false;
|
isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로
|
||||||
isResting = false;
|
isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
|
||||||
StopAllCoroutines();
|
StopAllCoroutines(); // 중단할거에요 -> 실행 중인 모든 코루틴을
|
||||||
|
|
||||||
OnStartHit();
|
OnStartHit(); // 함수를 실행할거에요 -> 자식 클래스의 추가 피격 처리 OnStartHit을
|
||||||
|
|
||||||
if (agent && agent.isOnNavMesh)
|
if (agent && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 작동 중이라면
|
||||||
{
|
{
|
||||||
agent.isStopped = true;
|
agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
|
||||||
agent.velocity = Vector3.zero;
|
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
|
||||||
}
|
}
|
||||||
|
|
||||||
animator.Play(Monster_GetDamage, 0, 0f);
|
animator.Play(Monster_GetDamage, 0, 0f); // 재생할거에요 -> 피격 애니메이션(Monster_GetDamage)을
|
||||||
if (hitEffect) hitEffect.Play();
|
if (hitEffect) hitEffect.Play(); // 조건이 맞으면 실행할거에요 -> 피격 이펙트가 있다면 재생하기를
|
||||||
if (hitSound) audioSource.PlayOneShot(hitSound);
|
if (hitSound) audioSource.PlayOneShot(hitSound); // 조건이 맞으면 실행할거에요 -> 피격 사운드가 있다면 재생하기를
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void OnStartHit() { }
|
protected virtual void OnStartHit() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 피격 시작 함수 OnStartHit을
|
||||||
|
|
||||||
public virtual void OnHitEnd()
|
public virtual void OnHitEnd() // 함수를 선언할거에요 -> 피격 연출이 끝났을 때 호출할 OnHitEnd를
|
||||||
{
|
{
|
||||||
isHit = false;
|
isHit = false; // 상태를 바꿀거에요 -> 피격 중 상태를 거짓(false)으로
|
||||||
if (agent && agent.isOnNavMesh) agent.isStopped = false;
|
if (agent && agent.isOnNavMesh) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 다시 움직이게(false) 하기를
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void Die()
|
protected virtual void Die() // 함수를 선언할거에요 -> 몬스터 사망을 처리하는 Die를
|
||||||
{
|
{
|
||||||
if (isDead) return;
|
if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽은 상태라면
|
||||||
isDead = true;
|
isDead = true; // 상태를 바꿀거에요 -> 사망 상태를 참(true)으로
|
||||||
IsAggroed = false;
|
IsAggroed = false; // 상태를 바꿀거에요 -> 어그로 상태를 거짓(false)으로
|
||||||
|
|
||||||
OnDie();
|
OnDie(); // 함수를 실행할거에요 -> 자식 클래스의 추가 사망 처리 OnDie를
|
||||||
|
|
||||||
OnMonsterKilled?.Invoke(expReward);
|
OnMonsterKilled?.Invoke(expReward); // 이벤트를 알릴거에요 -> 몬스터 처치와 경험치 보상(expReward)을
|
||||||
Collider col = GetComponent<Collider>();
|
Collider col = GetComponent<Collider>(); // 컴포넌트를 가져올거에요 -> 내 몸의 콜라이더를 col에
|
||||||
if (col != null) col.enabled = false;
|
if (col != null) col.enabled = false; // 조건이 맞으면 실행할거에요 -> 콜라이더가 있다면 끄기(false)를 (시체 밟기 방지)
|
||||||
|
|
||||||
if (agent && agent.isOnNavMesh)
|
if (agent && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 작동 중이라면
|
||||||
{
|
{
|
||||||
agent.isStopped = true;
|
agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
|
||||||
agent.velocity = Vector3.zero;
|
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
|
||||||
}
|
}
|
||||||
|
|
||||||
animator.Play(Monster_Die, 0, 0f);
|
animator.Play(Monster_Die, 0, 0f); // 재생할거에요 -> 사망 애니메이션(Monster_Die)을
|
||||||
if (deathSound) audioSource.PlayOneShot(deathSound);
|
if (deathSound) audioSource.PlayOneShot(deathSound); // 조건이 맞으면 실행할거에요 -> 사망 사운드가 있다면 재생하기를
|
||||||
Invoke("ReturnToPool", 1.5f);
|
Invoke("ReturnToPool", 1.5f); // 예약을 걸거에요 -> 1.5초 뒤에 풀로 반환하는 ReturnToPool 함수를
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void OnDie() { }
|
protected virtual void OnDie() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 사망 처리 함수 OnDie를
|
||||||
|
|
||||||
public bool IsDead => isDead;
|
public bool IsDead => isDead; // 프로퍼티를 선언할거에요 -> 외부에서 사망 여부를 확인할 수 있는 IsDead를
|
||||||
protected void ReturnToPool() => gameObject.SetActive(false);
|
protected void ReturnToPool() => gameObject.SetActive(false); // 함수를 선언할거에요 -> 오브젝트를 비활성화해서 풀로 돌려보내는 ReturnToPool을
|
||||||
|
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
// ⭐ 공격 이벤트
|
// ⭐ 공격 이벤트
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
public virtual void OnAttackStart()
|
// ⭐ [핵심 수정] 자식에서 재정의할 수 있도록 public virtual로 선언
|
||||||
|
public virtual void OnAttackStart() // 함수를 선언할거에요 -> 공격 시작 시 호출되는 OnAttackStart를
|
||||||
{
|
{
|
||||||
isAttacking = true;
|
isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로
|
||||||
isResting = false;
|
isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
|
||||||
|
|
||||||
if (myWeapon != null)
|
if (myWeapon != null) // 조건이 맞으면 실행할거에요 -> 무기가 있다면
|
||||||
{
|
{
|
||||||
myWeapon.EnableHitBox();
|
myWeapon.EnableHitBox(); // 함수를 실행할거에요 -> 무기의 공격 판정을 켜는 EnableHitBox를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void OnAttackEnd()
|
// ⭐ [핵심 수정] 자식에서 재정의할 수 있도록 public virtual로 선언
|
||||||
|
public virtual void OnAttackEnd() // 함수를 선언할거에요 -> 공격 종료 시 호출되는 OnAttackEnd를
|
||||||
{
|
{
|
||||||
if (myWeapon != null)
|
if (myWeapon != null) // 조건이 맞으면 실행할거에요 -> 무기가 있다면
|
||||||
{
|
{
|
||||||
myWeapon.DisableHitBox();
|
myWeapon.DisableHitBox(); // 함수를 실행할거에요 -> 무기의 공격 판정을 끄는 DisableHitBox를
|
||||||
}
|
}
|
||||||
|
|
||||||
isAttacking = false;
|
isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로
|
||||||
if (!isDead && !isHit) StartCoroutine(RestAfterAttack());
|
if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 조건이 맞으면 실행할거에요 -> 살아있고 안 맞았다면 휴식 코루틴(RestAfterAttack)을
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual IEnumerator RestAfterAttack()
|
protected virtual IEnumerator RestAfterAttack() // 코루틴 함수를 선언할거에요 -> 공격 후 잠시 쉬는 RestAfterAttack을
|
||||||
{
|
{
|
||||||
isResting = true;
|
isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참(true)으로
|
||||||
yield return new WaitForSeconds(attackRestDuration);
|
yield return new WaitForSeconds(attackRestDuration); // 기다릴거에요 -> 휴식 시간(attackRestDuration)만큼
|
||||||
isResting = false;
|
isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
|
||||||
}
|
}
|
||||||
|
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
// ⭐ 상태 이상 시스템 (추가된 부분)
|
// ⭐ 상태 이상 시스템
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
/// <summary>
|
public void ApplyStatusEffect(StatusEffectType type, float damage, float duration) // 함수를 선언할거에요 -> 상태이상을 적용하는 ApplyStatusEffect를
|
||||||
/// 상태이상 적용 (PlayerArrow에서 호출)
|
|
||||||
/// </summary>
|
|
||||||
public void ApplyStatusEffect(StatusEffectType type, float damage, float duration)
|
|
||||||
{
|
{
|
||||||
if (isDead) return;
|
if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면
|
||||||
|
|
||||||
switch (type)
|
switch (type) // 분기할거에요 -> 상태이상 종류(type)에 따라
|
||||||
{
|
{
|
||||||
case StatusEffectType.Burn:
|
case StatusEffectType.Burn: // 조건이 맞으면 실행할거에요 -> 화상(Burn)이라면
|
||||||
StartCoroutine(BurnCoroutine(damage, duration));
|
StartCoroutine(BurnCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 화상 효과를 주는 BurnCoroutine을
|
||||||
break;
|
break;
|
||||||
case StatusEffectType.Slow:
|
case StatusEffectType.Slow: // 조건이 맞으면 실행할거에요 -> 슬로우(Slow)라면
|
||||||
StartCoroutine(SlowCoroutine(damage, duration));
|
StartCoroutine(SlowCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 느리게 만드는 SlowCoroutine을
|
||||||
break;
|
break;
|
||||||
case StatusEffectType.Poison:
|
case StatusEffectType.Poison: // 조건이 맞으면 실행할거에요 -> 독(Poison)이라면
|
||||||
StartCoroutine(PoisonCoroutine(damage, duration));
|
StartCoroutine(PoisonCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 독 효과를 주는 PoisonCoroutine을
|
||||||
break;
|
break;
|
||||||
case StatusEffectType.Shock:
|
case StatusEffectType.Shock: // 조건이 맞으면 실행할거에요 -> 충격(Shock)이라면
|
||||||
TakeDamage(damage); // 충격 데미지 즉시 적용
|
TakeDamage(damage); // 함수를 실행할거에요 -> 충격 데미지를 즉시 적용하기를
|
||||||
StartCoroutine(StunCoroutine(0.5f)); // 0.5초 스턴
|
StartCoroutine(StunCoroutine(0.5f)); // 코루틴을 실행할거에요 -> 0.5초간 기절시키는 StunCoroutine을
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private IEnumerator BurnCoroutine(float tickDamage, float duration) // 코루틴 함수를 선언할거에요 -> 화상 효과를 처리할 BurnCoroutine을
|
||||||
/// 화상: 일정 시간 동안 0.5초마다 틱 데미지
|
|
||||||
/// </summary>
|
|
||||||
private IEnumerator BurnCoroutine(float tickDamage, float duration)
|
|
||||||
{
|
{
|
||||||
float elapsed = 0f;
|
float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로
|
||||||
float tickInterval = 0.5f;
|
float tickInterval = 0.5f; // 변수를 초기화할거에요 -> 데미지 간격을 0.5초로
|
||||||
while (elapsed < duration)
|
while (elapsed < duration) // 반복할거에요 -> 경과 시간이 지속 시간보다 작을 동안
|
||||||
{
|
{
|
||||||
TakeDamage(tickDamage * tickInterval);
|
TakeDamage(tickDamage * tickInterval); // 함수를 실행할거에요 -> 틱 데미지를 입히는 TakeDamage를
|
||||||
yield return new WaitForSeconds(tickInterval);
|
yield return new WaitForSeconds(tickInterval); // 기다릴거에요 -> 0.5초의 시간을
|
||||||
elapsed += tickInterval;
|
elapsed += tickInterval; // 값을 더할거에요 -> 경과 시간에 0.5초를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private IEnumerator SlowCoroutine(float amount, float duration) // 코루틴 함수를 선언할거에요 -> 슬로우 효과를 처리할 SlowCoroutine을
|
||||||
/// 슬로우: 이동속도를 일시적으로 감소
|
|
||||||
/// </summary>
|
|
||||||
private IEnumerator SlowCoroutine(float amount, float duration)
|
|
||||||
{
|
{
|
||||||
// NavMeshAgent가 있는 경우 속도 조절
|
if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
|
||||||
if (agent != null)
|
|
||||||
{
|
{
|
||||||
float originalSpeed = agent.speed;
|
float originalSpeed = agent.speed; // 값을 저장할거에요 -> 원래 속도를 originalSpeed에
|
||||||
agent.speed *= (1f - Mathf.Clamp01(amount / 100f));
|
agent.speed *= (1f - Mathf.Clamp01(amount / 100f)); // 값을 변경할거에요 -> 현재 속도를 비율(amount)만큼 줄여서
|
||||||
yield return new WaitForSeconds(duration);
|
yield return new WaitForSeconds(duration); // 기다릴거에요 -> 지속 시간(duration)만큼
|
||||||
agent.speed = originalSpeed; // 원래 속도로 복구
|
agent.speed = originalSpeed; // 값을 복구할거에요 -> 속도를 원래대로
|
||||||
}
|
}
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 에이전트가 없다면
|
||||||
{
|
{
|
||||||
// NavMeshAgent가 없으면 그냥 대기
|
yield return new WaitForSeconds(duration); // 기다릴거에요 -> 시간만 때우기를
|
||||||
yield return new WaitForSeconds(duration);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private IEnumerator PoisonCoroutine(float tickDamage, float duration) // 코루틴 함수를 선언할거에요 -> 독 효과를 처리할 PoisonCoroutine을
|
||||||
/// 독: 지속 데미지 (1초마다)
|
|
||||||
/// </summary>
|
|
||||||
private IEnumerator PoisonCoroutine(float tickDamage, float duration)
|
|
||||||
{
|
{
|
||||||
float elapsed = 0f;
|
float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로
|
||||||
float tickInterval = 1f;
|
float tickInterval = 1f; // 변수를 초기화할거에요 -> 데미지 간격을 1초로
|
||||||
while (elapsed < duration)
|
while (elapsed < duration) // 반복할거에요 -> 경과 시간이 지속 시간보다 작을 동안
|
||||||
{
|
{
|
||||||
TakeDamage(tickDamage * tickInterval);
|
TakeDamage(tickDamage * tickInterval); // 함수를 실행할거에요 -> 틱 데미지를 입히는 TakeDamage를
|
||||||
yield return new WaitForSeconds(tickInterval);
|
yield return new WaitForSeconds(tickInterval); // 기다릴거에요 -> 1초의 시간을
|
||||||
elapsed += tickInterval;
|
elapsed += tickInterval; // 값을 더할거에요 -> 경과 시간에 1초를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private IEnumerator StunCoroutine(float duration) // 코루틴 함수를 선언할거에요 -> 기절 효과를 처리할 StunCoroutine을
|
||||||
/// 스턴: 짧은 시간 동안 행동 정지
|
|
||||||
/// </summary>
|
|
||||||
private IEnumerator StunCoroutine(float duration)
|
|
||||||
{
|
{
|
||||||
if (agent != null)
|
if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
|
||||||
{
|
{
|
||||||
agent.isStopped = true;
|
agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
|
||||||
if (animator != null) animator.speed = 0; // 애니메이션도 멈춤 (선택사항)
|
if (animator != null) animator.speed = 0; // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 재생 속도를 0으로 (얼음 땡)
|
||||||
|
|
||||||
yield return new WaitForSeconds(duration);
|
yield return new WaitForSeconds(duration); // 기다릴거에요 -> 지속 시간(duration)만큼
|
||||||
|
|
||||||
if (!isDead)
|
if (!isDead) // 조건이 맞으면 실행할거에요 -> 아직 살아있다면
|
||||||
{
|
{
|
||||||
agent.isStopped = false;
|
agent.isStopped = false; // 명령을 내릴거에요 -> 이동 멈춤을 풀라(false)고
|
||||||
if (animator != null) animator.speed = 1;
|
if (animator != null) animator.speed = 1; // 조건이 맞으면 실행할거에요 -> 애니메이션 속도를 정상(1)으로
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 에이전트가 없다면
|
||||||
{
|
{
|
||||||
yield return new WaitForSeconds(duration);
|
yield return new WaitForSeconds(duration); // 기다릴거에요 -> 시간만 때우기를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,185 +1,182 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using UnityEngine.AI;
|
using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
|
||||||
using System.Collections;
|
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
||||||
using System.Collections.Generic; // ✅ 리스트 사용을 위해 필수 추가
|
using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
|
||||||
|
|
||||||
public class MeleeMonster : MonsterClass
|
public class MeleeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 MeleeMonster를
|
||||||
{
|
{
|
||||||
[Header("=== 근접 공격 설정 ===")]
|
[Header("=== 근접 공격 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 근접 공격 설정 === 을
|
||||||
//[SerializeField] private MonsterWeapon myWeapon; // ✅ 주석 해제됨 (이제 무기 연결 가능!)
|
[SerializeField] private float attackRange = 2f; // 변수를 선언할거에요 -> 공격 사거리(2.0)를 attackRange에
|
||||||
[SerializeField] private float attackRange = 2f;
|
[SerializeField] private float attackDelay = 1.5f; // 변수를 선언할거에요 -> 공격 딜레이(1.5초)를 attackDelay에
|
||||||
[SerializeField] private float attackDelay = 1.5f;
|
|
||||||
|
|
||||||
// 🎁 [수정] 단일 아이템 -> 리스트로 변경
|
[Header("=== 드랍 아이템 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 드랍 아이템 설정 === 을
|
||||||
[Header("=== 드랍 아이템 설정 ===")]
|
[Tooltip("드랍 가능한 아이템들을 여기에 여러 개 등록하세요. (랜덤 1개 드랍)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[Tooltip("드랍 가능한 아이템들을 여기에 여러 개 등록하세요. (랜덤 1개 드랍)")]
|
[SerializeField] private List<GameObject> dropItemPrefabs; // 리스트를 선언할거에요 -> 드랍할 아이템 프리팹 목록을 dropItemPrefabs에
|
||||||
[SerializeField] private List<GameObject> dropItemPrefabs;
|
|
||||||
|
|
||||||
[Tooltip("아이템이 나올 확률 (0 ~ 100%)")]
|
[Tooltip("아이템이 나올 확률 (0 ~ 100%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[Range(0, 100)][SerializeField] private float dropChance = 30f; // 기본 30%
|
[Range(0, 100)][SerializeField] private float dropChance = 30f; // 변수를 선언할거에요 -> 드랍 확률(30%)을 dropChance에
|
||||||
|
|
||||||
private float lastAttackTime;
|
private float lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간을 lastAttackTime에
|
||||||
|
|
||||||
[Header("공격 / 이동 애니메이션")]
|
[Header("공격 / 이동 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 공격 / 이동 애니메이션 을
|
||||||
[SerializeField] private string[] attackAnimations = { "Monster_Attack_1" };
|
[SerializeField] private string[] attackAnimations = { "Monster_Attack_1" }; // 배열을 선언할거에요 -> 공격 애니메이션 이름들을 attackAnimations에
|
||||||
[SerializeField] private string Monster_Walk = "Monster_Walk";
|
[SerializeField] private string Monster_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 Monster_Walk에
|
||||||
|
|
||||||
[Header("AI 상세 설정")]
|
[Header("AI 상세 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 상세 설정 을
|
||||||
[SerializeField] private float stopBuffer = 0.3f;
|
[SerializeField] private float stopBuffer = 0.3f; // 변수를 선언할거에요 -> 정지 여유 거리(0.3)를 stopBuffer에
|
||||||
[SerializeField] private float patrolRadius = 5f;
|
[SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에
|
||||||
[SerializeField] private float patrolInterval = 2f;
|
[SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 대기 시간(2.0)을 patrolInterval에
|
||||||
|
|
||||||
private float nextPatrolTime;
|
private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에
|
||||||
private float repathInterval = 0.3f;
|
private float repathInterval = 0.3f; // 변수를 선언할거에요 -> 경로 갱신 주기(0.3초)를 repathInterval에
|
||||||
private float nextRepathTime;
|
private float nextRepathTime; // 변수를 선언할거에요 -> 다음 경로 갱신 시간을 nextRepathTime에
|
||||||
private int attackIndex;
|
private int attackIndex; // 변수를 선언할거에요 -> 현재 공격 애니메이션 인덱스를 attackIndex에
|
||||||
private bool isPlayerInZone;
|
private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어가 감지 구역에 있는지 여부를 isPlayerInZone에
|
||||||
|
|
||||||
protected override void Init()
|
protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을
|
||||||
{
|
{
|
||||||
if (agent != null) agent.stoppingDistance = attackRange - 0.4f;
|
if (agent != null) agent.stoppingDistance = attackRange - 0.4f; // 조건이 맞으면 설정할거에요 -> 에이전트 정지 거리를 사거리보다 약간 짧게
|
||||||
if (animator != null) animator.applyRootMotion = false;
|
if (animator != null) animator.applyRootMotion = false; // 조건이 맞으면 설정할거에요 -> 애니메이션 이동(Root Motion)을 끄기로
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnResetStats()
|
protected override void OnResetStats() // 함수를 덮어씌워 실행할거에요 -> 스탯 초기화 로직인 OnResetStats를
|
||||||
{
|
{
|
||||||
if (myWeapon != null)
|
if (myWeapon != null) // 조건이 맞으면 실행할거에요 -> 무기가 있다면
|
||||||
{
|
{
|
||||||
myWeapon.SetDamage(attackDamage);
|
myWeapon.SetDamage(attackDamage); // 함수를 실행할거에요 -> 무기 공격력을 몬스터 공격력으로 설정을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ExecuteAILogic()
|
protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을
|
||||||
{
|
{
|
||||||
if (isHit || isAttacking || isResting) return;
|
if (isHit || isAttacking || isResting) return; // 조건이 맞으면 중단할거에요 -> 피격, 공격, 휴식 중이라면
|
||||||
|
|
||||||
float distance = Vector3.Distance(transform.position, playerTransform.position);
|
float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 나와 플레이어 사이의 거리를 distance에
|
||||||
|
|
||||||
if (isPlayerInZone || distance <= attackRange * 2f)
|
if (isPlayerInZone || distance <= attackRange * 2f) // 조건이 맞으면 실행할거에요 -> 감지 구역 안이거나 거리가 가까우면
|
||||||
HandlePlayerTarget();
|
HandlePlayerTarget(); // 함수를 실행할거에요 -> 추격 및 공격 처리를 하는 HandlePlayerTarget을
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 멀리 있다면
|
||||||
Patrol();
|
Patrol(); // 함수를 실행할거에요 -> 주변을 배회하는 Patrol을
|
||||||
|
|
||||||
UpdateMovementAnimation();
|
UpdateMovementAnimation(); // 함수를 실행할거에요 -> 애니메이션 상태를 갱신하는 UpdateMovementAnimation을
|
||||||
}
|
}
|
||||||
|
|
||||||
void HandlePlayerTarget()
|
void HandlePlayerTarget() // 함수를 선언할거에요 -> 타겟 처리 로직인 HandlePlayerTarget을
|
||||||
{
|
{
|
||||||
float distance = Vector3.Distance(transform.position, playerTransform.position);
|
float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를
|
||||||
|
|
||||||
if (distance <= attackRange - stopBuffer)
|
if (distance <= attackRange - stopBuffer) // 조건이 맞으면 실행할거에요 -> 사거리 안쪽으로 충분히 들어왔다면
|
||||||
TryAttack();
|
TryAttack(); // 함수를 실행할거에요 -> 공격을 시도하는 TryAttack을
|
||||||
else if (Time.time >= nextRepathTime)
|
else if (Time.time >= nextRepathTime) // 조건이 맞으면 실행할거에요 -> 경로 갱신 시간이 되었다면
|
||||||
{
|
{
|
||||||
if (agent.isOnNavMesh) agent.SetDestination(playerTransform.position);
|
if (agent.isOnNavMesh) agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로
|
||||||
nextRepathTime = Time.time + repathInterval;
|
nextRepathTime = Time.time + repathInterval; // 값을 갱신할거에요 -> 다음 경로 갱신 시간을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TryAttack()
|
void TryAttack() // 함수를 선언할거에요 -> 공격을 수행하는 TryAttack을
|
||||||
{
|
{
|
||||||
if (Time.time < lastAttackTime + attackDelay) return;
|
if (Time.time < lastAttackTime + attackDelay) return; // 조건이 맞으면 중단할거에요 -> 아직 쿨타임이 안 지났다면
|
||||||
lastAttackTime = Time.time;
|
lastAttackTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 공격 시간으로
|
||||||
|
|
||||||
string attackName = attackAnimations[attackIndex];
|
string attackName = attackAnimations[attackIndex]; // 값을 가져올거에요 -> 현재 인덱스의 공격 애니메이션 이름을
|
||||||
attackIndex = (attackIndex + 1) % attackAnimations.Length;
|
attackIndex = (attackIndex + 1) % attackAnimations.Length; // 값을 갱신할거에요 -> 다음 공격 인덱스로 (순환)
|
||||||
|
|
||||||
isAttacking = true;
|
isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로
|
||||||
|
|
||||||
if (agent.isOnNavMesh)
|
if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 바닥에 있다면
|
||||||
{
|
{
|
||||||
agent.isStopped = true;
|
agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고
|
||||||
agent.velocity = Vector3.zero;
|
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
|
||||||
}
|
}
|
||||||
|
|
||||||
animator.Play(attackName, 0, 0f);
|
animator.Play(attackName, 0, 0f); // 재생할거에요 -> 공격 애니메이션을 처음부터
|
||||||
}
|
}
|
||||||
|
|
||||||
void UpdateMovementAnimation()
|
void UpdateMovementAnimation() // 함수를 선언할거에요 -> 이동 애니메이션을 제어하는 UpdateMovementAnimation을
|
||||||
{
|
{
|
||||||
if (isAttacking || isHit || isResting) return;
|
if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면
|
||||||
|
|
||||||
if (agent.velocity.magnitude < 0.1f)
|
if (agent.velocity.magnitude < 0.1f) // 조건이 맞으면 실행할거에요 -> 거의 멈춰 있다면
|
||||||
animator.Play(Monster_Idle);
|
animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션을
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 움직이고 있다면
|
||||||
animator.Play(Monster_Walk);
|
animator.Play(Monster_Walk); // 재생할거에요 -> 걷기 애니메이션을
|
||||||
}
|
}
|
||||||
|
|
||||||
void Patrol()
|
void Patrol() // 함수를 선언할거에요 -> 순찰 로직인 Patrol을
|
||||||
{
|
{
|
||||||
if (Time.time < nextPatrolTime) return;
|
if (Time.time < nextPatrolTime) return; // 조건이 맞으면 중단할거에요 -> 아직 대기 시간이라면
|
||||||
|
|
||||||
Vector3 randomPoint = transform.position + new Vector3(
|
Vector3 randomPoint = transform.position + new Vector3( // 벡터를 계산할거에요 -> 현재 위치 주변의 랜덤한 지점을
|
||||||
Random.Range(-patrolRadius, patrolRadius),
|
Random.Range(-patrolRadius, patrolRadius),
|
||||||
0,
|
0,
|
||||||
Random.Range(-patrolRadius, patrolRadius)
|
Random.Range(-patrolRadius, patrolRadius)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas))
|
if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> 랜덤 지점이 NavMesh 위라면
|
||||||
if (agent.isOnNavMesh) agent.SetDestination(hit.position);
|
if (agent.isOnNavMesh) agent.SetDestination(hit.position); // 명령을 내릴거에요 -> 그 지점으로 이동하라고
|
||||||
|
|
||||||
nextPatrolTime = Time.time + patrolInterval;
|
nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnAttackStart()
|
public override void OnAttackStart() // 함수를 덮어씌워 실행할거에요 -> 공격 시작 시 호출되는 OnAttackStart를
|
||||||
{
|
{
|
||||||
isAttacking = true;
|
isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로
|
||||||
isResting = false;
|
isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
|
||||||
if (myWeapon != null) myWeapon.EnableHitBox();
|
if (myWeapon != null) myWeapon.EnableHitBox(); // 조건이 맞으면 실행할거에요 -> 무기 공격 판정을 켜기를
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnAttackEnd()
|
public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 시 호출되는 OnAttackEnd를
|
||||||
{
|
{
|
||||||
if (myWeapon != null) myWeapon.DisableHitBox();
|
if (myWeapon != null) myWeapon.DisableHitBox(); // 조건이 맞으면 실행할거에요 -> 무기 공격 판정을 끄기를
|
||||||
isAttacking = false;
|
isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로
|
||||||
if (!isDead && !isHit) StartCoroutine(RestAfterAttack());
|
if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 조건이 맞으면 실행할거에요 -> 살아있고 안 맞았다면 휴식 코루틴을
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerator RestAfterAttack()
|
protected override IEnumerator RestAfterAttack() // 코루틴 함수를 덮어씌워 실행할거에요 -> 공격 후 휴식하는 RestAfterAttack을
|
||||||
{
|
{
|
||||||
isResting = true;
|
isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참(true)으로
|
||||||
yield return new WaitForSeconds(attackRestDuration);
|
yield return new WaitForSeconds(attackRestDuration); // 기다릴거에요 -> 휴식 시간만큼
|
||||||
isResting = false;
|
isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnStartHit()
|
protected override void OnStartHit() // 함수를 덮어씌워 실행할거에요 -> 피격 시작 시 호출되는 OnStartHit을
|
||||||
{
|
{
|
||||||
if (myWeapon != null) myWeapon.DisableHitBox();
|
if (myWeapon != null) myWeapon.DisableHitBox(); // 조건이 맞으면 실행할거에요 -> 피격 당했으니 공격 판정을 끄기를
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🎲 [핵심 수정] 리스트에서 랜덤 뽑기 + 확률 체크
|
// 🎲 [핵심 수정] 리스트에서 랜덤 뽑기 + 확률 체크
|
||||||
protected override void OnDie()
|
protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 사망 시 호출되는 OnDie를
|
||||||
{
|
{
|
||||||
if (myWeapon != null) myWeapon.DisableHitBox();
|
if (myWeapon != null) myWeapon.DisableHitBox(); // 조건이 맞으면 실행할거에요 -> 공격 판정을 끄기를
|
||||||
|
|
||||||
// 1. 리스트에 아이템이 하나라도 있는지 확인
|
// 1. 리스트에 아이템이 하나라도 있는지 확인
|
||||||
if (dropItemPrefabs != null && dropItemPrefabs.Count > 0)
|
if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) // 조건이 맞으면 실행할거에요 -> 드랍 테이블이 비어있지 않다면
|
||||||
{
|
{
|
||||||
// 2. 확률 체크 (0 ~ 100)
|
// 2. 확률 체크 (0 ~ 100)
|
||||||
float randomValue = Random.Range(0f, 100f);
|
float randomValue = Random.Range(0f, 100f); // 값을 뽑을거에요 -> 0부터 100 사이의 랜덤값을
|
||||||
|
|
||||||
if (randomValue <= dropChance) // 당첨!
|
if (randomValue <= dropChance) // 당첨! // 조건이 맞으면 실행할거에요 -> 랜덤값이 드랍 확률 이하라면
|
||||||
{
|
{
|
||||||
// 3. 리스트에서 랜덤하게 하나 뽑기 (0번 ~ 마지막 번호 중 하나)
|
// 3. 리스트에서 랜덤하게 하나 뽑기 (0번 ~ 마지막 번호 중 하나)
|
||||||
int randomIndex = Random.Range(0, dropItemPrefabs.Count);
|
int randomIndex = Random.Range(0, dropItemPrefabs.Count); // 값을 뽑을거에요 -> 아이템 리스트 인덱스를 랜덤으로
|
||||||
GameObject selectedItem = dropItemPrefabs[randomIndex];
|
GameObject selectedItem = dropItemPrefabs[randomIndex]; // 오브젝트를 가져올거에요 -> 선택된 아이템 프리팹을
|
||||||
|
|
||||||
if (selectedItem != null)
|
if (selectedItem != null) // 조건이 맞으면 실행할거에요 -> 아이템이 유효하다면
|
||||||
{
|
{
|
||||||
Instantiate(selectedItem, transform.position + Vector3.up * 0.5f, Quaternion.identity);
|
Instantiate(selectedItem, transform.position + Vector3.up * 0.5f, Quaternion.identity); // 생성할거에요 -> 아이템을 몬스터 위치에
|
||||||
// Debug.Log($"🎉 아이템 드랍! ({selectedItem.name})");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTriggerEnter(Collider other)
|
private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 트리거에 들어왔을 때 OnTriggerEnter를
|
||||||
{
|
{
|
||||||
if (other.CompareTag("Player")) isPlayerInZone = true;
|
if (other.CompareTag("Player")) isPlayerInZone = true; // 조건이 맞으면 실행할거에요 -> 플레이어라면 감지 구역 진입 상태를 참(true)으로
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTriggerExit(Collider other)
|
private void OnTriggerExit(Collider other) // 함수를 실행할거에요 -> 트리거에서 나갔을 때 OnTriggerExit을
|
||||||
{
|
{
|
||||||
if (other.CompareTag("Player")) isPlayerInZone = false;
|
if (other.CompareTag("Player")) isPlayerInZone = false; // 조건이 맞으면 실행할거에요 -> 플레이어라면 감지 구역 진입 상태를 거짓(false)으로
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,213 +1,208 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using UnityEngine.AI;
|
using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
|
||||||
using System.Collections;
|
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 통합 원거리 몬스터 (키 작은 플레이어 머리 조준 기능 추가)
|
/// 통합 원거리 몬스터 (키 작은 플레이어 머리 조준 기능 추가)
|
||||||
/// 파일 이름을 반드시 [ UniversalRangedMonster.cs ] 로 맞춰주세요!
|
/// 파일 이름을 반드시 [ UniversalRangedMonster.cs ] 로 맞춰주세요!
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class UniversalRangedMonster : MonsterClass
|
public class UniversalRangedMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 UniversalRangedMonster를
|
||||||
{
|
{
|
||||||
public enum AttackStyle { Straight, Lob }
|
public enum AttackStyle { Straight, Lob } // 열거형을 정의할거에요 -> 공격 방식(직선, 곡사)을 정의하는 AttackStyle을
|
||||||
|
|
||||||
[Header("=== 🏹 공격 스타일 선택 ===")]
|
[Header("=== 🏹 공격 스타일 선택 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 🏹 공격 스타일 선택 === 을
|
||||||
[SerializeField] private AttackStyle attackStyle = AttackStyle.Straight;
|
[SerializeField] private AttackStyle attackStyle = AttackStyle.Straight; // 변수를 선언할거에요 -> 공격 스타일(기본 직선)을 attackStyle에
|
||||||
|
|
||||||
[Header("공통 설정")]
|
[Header("공통 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 공통 설정 을
|
||||||
[SerializeField] private GameObject projectilePrefab;
|
[SerializeField] private GameObject projectilePrefab; // 변수를 선언할거에요 -> 발사체 프리팹을 projectilePrefab에
|
||||||
[SerializeField] private Transform firePoint;
|
[SerializeField] private Transform firePoint; // 변수를 선언할거에요 -> 발사 위치를 firePoint에
|
||||||
[SerializeField] private float attackRange = 10f;
|
[SerializeField] private float attackRange = 10f; // 변수를 선언할거에요 -> 공격 사거리(10.0)를 attackRange에
|
||||||
[SerializeField] private float attackDelay = 2f;
|
[SerializeField] private float attackDelay = 2f; // 변수를 선언할거에요 -> 공격 딜레이(2.0초)를 attackDelay에
|
||||||
[SerializeField] private float detectRange = 15f;
|
[SerializeField] private float detectRange = 15f; // 변수를 선언할거에요 -> 인식 거리(15.0)를 detectRange에
|
||||||
|
|
||||||
[Header("🔹 직선 발사 설정 (활/총)")]
|
[Header("🔹 직선 발사 설정 (활/총)")] // 인스펙터 창에 제목을 표시할거에요 -> 🔹 직선 발사 설정 (활/총) 을
|
||||||
[SerializeField] private float projectileSpeed = 20f;
|
[SerializeField] private float projectileSpeed = 20f; // 변수를 선언할거에요 -> 투사체 속도(20.0)를 projectileSpeed에
|
||||||
[SerializeField] private float minDistance = 5f;
|
[SerializeField] private float minDistance = 5f; // 변수를 선언할거에요 -> 최소 거리(도망가는 거리)를 minDistance에
|
||||||
|
|
||||||
[Header("🔸 곡사 투척 설정 (돌/폭탄)")]
|
[Header("🔸 곡사 투척 설정 (돌/폭탄)")] // 인스펙터 창에 제목을 표시할거에요 -> 🔸 곡사 투척 설정 (돌/폭탄) 을
|
||||||
[Tooltip("체크하면 거리/높이를 계산해서 정확히 맞춥니다.")]
|
[Tooltip("체크하면 거리/높이를 계산해서 정확히 맞춥니다.")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[SerializeField] private bool usePreciseLob = true;
|
[SerializeField] private bool usePreciseLob = true; // 변수를 선언할거에요 -> 정밀 곡사 사용 여부를 usePreciseLob에
|
||||||
|
|
||||||
[Tooltip("던지는 각도 (45도가 최대 사거리)")]
|
[Tooltip("던지는 각도 (45도가 최대 사거리)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[Range(15f, 75f)][SerializeField] private float launchAngle = 45f;
|
[Range(15f, 75f)][SerializeField] private float launchAngle = 45f; // 변수를 선언할거에요 -> 발사 각도(45도)를 launchAngle에
|
||||||
|
|
||||||
// ⭐ [추가됨] 조준 높이 보정 (키 작은 플레이어 해결용)
|
// ⭐ [추가됨] 조준 높이 보정 (키 작은 플레이어 해결용)
|
||||||
[Tooltip("0이면 발가락, 1.0이면 명치, 1.5면 머리를 노립니다.")]
|
[Tooltip("0이면 발가락, 1.0이면 명치, 1.5면 머리를 노립니다.")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[SerializeField] private float aimHeight = 1.2f;
|
[SerializeField] private float aimHeight = 1.2f; // 변수를 선언할거에요 -> 조준 높이 오프셋(1.2)을 aimHeight에
|
||||||
|
|
||||||
[Header("🏃♂️ 도망 설정")]
|
[Header("🏃♂️ 도망 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 🏃♂️ 도망 설정 을
|
||||||
[SerializeField] private float fleeDistance = 5f;
|
[SerializeField] private float fleeDistance = 5f; // 변수를 선언할거에요 -> 도망가는 거리(5.0)를 fleeDistance에
|
||||||
|
|
||||||
[Header("애니메이션 & 기타")]
|
[Header("애니메이션 & 기타")] // 인스펙터 창에 제목을 표시할거에요 -> 애니메이션 & 기타 를
|
||||||
[SerializeField] private float throwForce = 15f; // (정밀 모드 꺼졌을 때 사용)
|
[SerializeField] private float throwForce = 15f; // 변수를 선언할거에요 -> 기본 투척 힘을 throwForce에
|
||||||
[SerializeField] private float throwUpward = 5f; // (정밀 모드 꺼졌을 때 사용)
|
[SerializeField] private float throwUpward = 5f; // 변수를 선언할거에요 -> 기본 상향 힘을 throwUpward에
|
||||||
[SerializeField] private float reloadTime = 2.0f;
|
[SerializeField] private float reloadTime = 2.0f; // 변수를 선언할거에요 -> 장전 시간(2.0초)을 reloadTime에
|
||||||
[SerializeField] private GameObject handModel;
|
[SerializeField] private GameObject handModel; // 변수를 선언할거에요 -> 손에 든 무기 모델을 handModel에
|
||||||
[SerializeField] private bool aimAtPlayer = true;
|
[SerializeField] private bool aimAtPlayer = true; // 변수를 선언할거에요 -> 플레이어 조준 여부를 aimAtPlayer에
|
||||||
|
|
||||||
[SerializeField] private string attackAnim = "Monster_Attack";
|
[SerializeField] private string attackAnim = "Monster_Attack"; // 변수를 선언할거에요 -> 공격 애니메이션 이름을 attackAnim에
|
||||||
[SerializeField] private string walkAnim = "Monster_Walk";
|
[SerializeField] private string walkAnim = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 walkAnim에
|
||||||
[SerializeField] private float patrolRadius = 5f;
|
[SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에
|
||||||
[SerializeField] private float patrolInterval = 3f;
|
[SerializeField] private float patrolInterval = 3f; // 변수를 선언할거에요 -> 순찰 간격(3.0초)을 patrolInterval에
|
||||||
|
|
||||||
private float lastAttackTime;
|
private float lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간을 lastAttackTime에
|
||||||
private float nextPatrolTime;
|
private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에
|
||||||
// private bool isPlayerInZone;
|
private bool isReloading = false; // 변수를 선언할거에요 -> 장전 중 여부를 isReloading에
|
||||||
private bool isReloading = false;
|
|
||||||
|
|
||||||
protected override void Init()
|
protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을
|
||||||
{
|
{
|
||||||
if (agent != null) { agent.stoppingDistance = attackRange * 0.8f; agent.speed = 3.5f; }
|
if (agent != null) { agent.stoppingDistance = attackRange * 0.8f; agent.speed = 3.5f; } // 조건이 맞으면 설정할거에요 -> 정지 거리를 사거리의 80%로, 속도를 3.5로
|
||||||
if (animator != null) animator.applyRootMotion = false;
|
if (animator != null) animator.applyRootMotion = false; // 조건이 맞으면 설정할거에요 -> 애니메이션 이동을 끄기로
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ExecuteAILogic()
|
protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을
|
||||||
{
|
{
|
||||||
if (isHit || isAttacking || isResting || isReloading) return;
|
if (isHit || isAttacking || isResting || isReloading) return; // 조건이 맞으면 중단할거에요 -> 피격, 공격, 휴식, 장전 중이라면
|
||||||
if (isAttacking && animator.GetCurrentAnimatorStateInfo(0).IsName(Monster_Idle)) OnAttackEnd();
|
if (isAttacking && animator.GetCurrentAnimatorStateInfo(0).IsName(Monster_Idle)) OnAttackEnd(); // 조건이 맞으면 실행할거에요 -> 공격 중인데 모션이 대기라면 강제로 공격 종료를
|
||||||
|
|
||||||
float dist = Vector3.Distance(transform.position, playerTransform.position);
|
float dist = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를
|
||||||
|
|
||||||
if (dist <= detectRange) HandleCombat(dist);
|
if (dist <= detectRange) HandleCombat(dist); // 조건이 맞으면 실행할거에요 -> 감지 거리 이내라면 전투 처리(HandleCombat)를
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 멀리 있다면
|
||||||
{
|
{
|
||||||
if (agent.hasPath && agent.destination == playerTransform.position) { agent.ResetPath(); nextPatrolTime = 0; }
|
if (agent.hasPath && agent.destination == playerTransform.position) { agent.ResetPath(); nextPatrolTime = 0; } // 조건이 맞으면 실행할거에요 -> 추격 중이었다면 경로를 취소하고 순찰을 준비하기를
|
||||||
Patrol();
|
Patrol(); // 함수를 실행할거에요 -> 순찰을 도는 Patrol을
|
||||||
UpdateMovementAnimation();
|
UpdateMovementAnimation(); // 함수를 실행할거에요 -> 애니메이션 갱신을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void HandleCombat(float dist)
|
void HandleCombat(float dist) // 함수를 선언할거에요 -> 거리별 전투 행동을 정하는 HandleCombat을
|
||||||
{
|
{
|
||||||
if (attackStyle == AttackStyle.Straight && dist < minDistance) RetreatFromPlayer();
|
if (attackStyle == AttackStyle.Straight && dist < minDistance) RetreatFromPlayer(); // 조건이 맞으면 실행할거에요 -> 직선 공격 타입이고 너무 가까우면 도망가기를
|
||||||
else if (dist <= attackRange) TryAttack();
|
else if (dist <= attackRange) TryAttack(); // 조건이 맞으면 실행할거에요 -> 사거리 안이라면 공격 시도를
|
||||||
else { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(playerTransform.position); UpdateMovementAnimation(); } }
|
else { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(playerTransform.position); UpdateMovementAnimation(); } } // 그 외엔 실행할거에요 -> 플레이어를 향해 추격하기를
|
||||||
}
|
}
|
||||||
|
|
||||||
void TryAttack()
|
void TryAttack() // 함수를 선언할거에요 -> 공격을 수행하는 TryAttack을
|
||||||
{
|
{
|
||||||
if (Time.time < lastAttackTime + attackDelay) return;
|
if (Time.time < lastAttackTime + attackDelay) return; // 조건이 맞으면 중단할거에요 -> 쿨타임이 안 지났다면
|
||||||
lastAttackTime = Time.time;
|
lastAttackTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 공격 시간으로
|
||||||
isAttacking = true;
|
isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로
|
||||||
|
|
||||||
if (agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; }
|
if (agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 조건이 맞으면 실행할거에요 -> 이동을 멈추고 제자리에 서기를
|
||||||
|
|
||||||
Vector3 lookDir = (playerTransform.position - transform.position).normalized;
|
Vector3 lookDir = (playerTransform.position - transform.position).normalized; // 벡터를 계산할거에요 -> 플레이어를 바라보는 방향을
|
||||||
lookDir.y = 0;
|
lookDir.y = 0; // 값을 바꿀거에요 -> 높이 차이를 무시하게
|
||||||
if (lookDir != Vector3.zero) transform.rotation = Quaternion.LookRotation(lookDir);
|
if (lookDir != Vector3.zero) transform.rotation = Quaternion.LookRotation(lookDir); // 회전시킬거에요 -> 플레이어 쪽을 보도록
|
||||||
|
|
||||||
animator.Play(attackAnim);
|
animator.Play(attackAnim); // 재생할거에요 -> 공격 애니메이션을
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnAttackStart()
|
public override void OnAttackStart() // 함수를 덮어씌워 실행할거에요 -> 공격 타이밍(애니메이션 이벤트)에 호출될 OnAttackStart를
|
||||||
{
|
{
|
||||||
if (!projectilePrefab || !firePoint) return;
|
if (!projectilePrefab || !firePoint) return; // 조건이 맞으면 중단할거에요 -> 프리팹이나 발사 위치가 없다면
|
||||||
|
|
||||||
GameObject obj = Instantiate(projectilePrefab, firePoint.position, transform.rotation);
|
GameObject obj = Instantiate(projectilePrefab, firePoint.position, transform.rotation); // 생성할거에요 -> 투사체를 발사 위치에
|
||||||
|
|
||||||
if (obj.TryGetComponent<Projectile>(out var proj))
|
if (obj.TryGetComponent<Projectile>(out var proj)) // 조건이 맞으면 실행할거에요 -> 투사체 스크립트가 있다면
|
||||||
{
|
{
|
||||||
float speed = (attackStyle == AttackStyle.Straight) ? projectileSpeed : 0f;
|
float speed = (attackStyle == AttackStyle.Straight) ? projectileSpeed : 0f; // 값을 결정할거에요 -> 직선이면 속도를, 곡사면 0(물리)을
|
||||||
proj.Initialize(transform.forward, speed, attackDamage);
|
proj.Initialize(transform.forward, speed, attackDamage); // 초기화할거에요 -> 방향, 속도, 데미지 정보를 전달해서
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attackStyle == AttackStyle.Lob)
|
if (attackStyle == AttackStyle.Lob) // 조건이 맞으면 실행할거에요 -> 곡사 공격 타입이라면
|
||||||
{
|
{
|
||||||
Rigidbody rb = obj.GetComponent<Rigidbody>();
|
Rigidbody rb = obj.GetComponent<Rigidbody>(); // 컴포넌트를 가져올거에요 -> 투사체의 리지드바디를
|
||||||
if (rb)
|
if (rb) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
|
||||||
{
|
{
|
||||||
rb.useGravity = true;
|
rb.useGravity = true; // 기능을 켤거에요 -> 중력 영향을 받도록
|
||||||
|
|
||||||
// ⭐ 목표 지점 설정 (플레이어 위치 + 높이 보정)
|
// ⭐ 목표 지점 설정 (플레이어 위치 + 높이 보정)
|
||||||
Vector3 targetPos = playerTransform.position + Vector3.up * aimHeight;
|
Vector3 targetPos = playerTransform.position + Vector3.up * aimHeight; // 위치를 계산할거에요 -> 플레이어 위치에 조준 높이를 더해서
|
||||||
|
|
||||||
if (usePreciseLob)
|
if (usePreciseLob) // 조건이 맞으면 실행할거에요 -> 정밀 곡사를 사용한다면
|
||||||
{
|
{
|
||||||
// 보정된 위치(targetPos)를 기준으로 탄도 계산
|
// 보정된 위치(targetPos)를 기준으로 탄도 계산
|
||||||
Vector3 velocity = CalculateLobVelocity(firePoint.position, targetPos, launchAngle);
|
Vector3 velocity = CalculateLobVelocity(firePoint.position, targetPos, launchAngle); // 계산할거에요 -> 목표에 도달하기 위한 물리 속도를
|
||||||
|
|
||||||
if (!float.IsNaN(velocity.x))
|
if (!float.IsNaN(velocity.x)) // 조건이 맞으면 실행할거에요 -> 계산 결과가 유효하다면
|
||||||
{
|
{
|
||||||
rb.velocity = velocity;
|
rb.velocity = velocity; // 값을 넣을거에요 -> 계산된 속도를 리지드바디에
|
||||||
}
|
}
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 계산 실패(도달 불가)라면
|
||||||
{
|
{
|
||||||
// 계산 실패 시 백업
|
// 계산 실패 시 백업
|
||||||
rb.AddForce(transform.forward * throwForce + Vector3.up * throwUpward, ForceMode.Impulse);
|
rb.AddForce(transform.forward * throwForce + Vector3.up * throwUpward, ForceMode.Impulse); // 힘을 가할거에요 -> 기본 힘으로 던지기를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 정밀 곡사가 아니라면
|
||||||
{
|
{
|
||||||
Vector3 dir = aimAtPlayer ? (targetPos - firePoint.position).normalized : transform.forward;
|
Vector3 dir = aimAtPlayer ? (targetPos - firePoint.position).normalized : transform.forward; // 방향을 결정할거에요 -> 타겟 방향 혹은 정면으로
|
||||||
// dir.y = 0; // 높이 조준을 위해 y제거 주석 처리 (원하면 해제)
|
rb.AddForce(dir * throwForce + Vector3.up * throwUpward, ForceMode.Impulse); // 힘을 가할거에요 -> 방향과 힘을 적용해서 던지기를
|
||||||
rb.AddForce(dir * throwForce + Vector3.up * throwUpward, ForceMode.Impulse);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handModel != null) { handModel.SetActive(false); StartCoroutine(ReloadRoutine()); }
|
if (handModel != null) { handModel.SetActive(false); StartCoroutine(ReloadRoutine()); } // 조건이 맞으면 실행할거에요 -> 손에 있는 무기를 숨기고 장전 코루틴을 시작하기를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector3 CalculateLobVelocity(Vector3 origin, Vector3 target, float angle)
|
Vector3 CalculateLobVelocity(Vector3 origin, Vector3 target, float angle) // 함수를 선언할거에요 -> 곡사 탄도 속도를 계산하는 CalculateLobVelocity를
|
||||||
{
|
{
|
||||||
Vector3 dir = target - origin;
|
Vector3 dir = target - origin; // 벡터를 계산할거에요 -> 목표까지의 거리 벡터를
|
||||||
float height = dir.y;
|
float height = dir.y; // 값을 저장할거에요 -> 높이 차이를
|
||||||
dir.y = 0;
|
dir.y = 0; // 값을 바꿀거에요 -> 수평 거리 계산을 위해 y를 0으로
|
||||||
float dist = dir.magnitude;
|
float dist = dir.magnitude; // 값을 저장할거에요 -> 수평 거리를
|
||||||
|
|
||||||
float a = angle * Mathf.Deg2Rad;
|
float a = angle * Mathf.Deg2Rad; // 값을 변환할거에요 -> 각도를 라디안으로
|
||||||
|
|
||||||
dir.y = dist * Mathf.Tan(a);
|
dir.y = dist * Mathf.Tan(a); // 값을 계산할거에요 -> 탄젠트를 이용해 높이를
|
||||||
dist += height / Mathf.Tan(a);
|
dist += height / Mathf.Tan(a); // 값을 보정할거에요 -> 높이 차이에 따른 거리 보정을
|
||||||
|
|
||||||
float gravity = Physics.gravity.magnitude;
|
float gravity = Physics.gravity.magnitude; // 값을 가져올거에요 -> 중력 가속도를
|
||||||
float velocitySq = (gravity * dist * dist) / (2 * Mathf.Pow(Mathf.Cos(a), 2) * (dist * Mathf.Tan(a) - height));
|
// 물리 공식: 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;
|
if (velocitySq <= 0) return Vector3.zero; // 조건이 맞으면 반환할거에요 -> 계산 불가라면 0 벡터를
|
||||||
|
|
||||||
float velocity = Mathf.Sqrt(velocitySq);
|
float velocity = Mathf.Sqrt(velocitySq); // 값을 계산할거에요 -> 제곱근을 구해서 실제 속도를
|
||||||
return dir.normalized * velocity;
|
return dir.normalized * velocity; // 반환할거에요 -> 방향 벡터에 속도를 곱해서
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... (나머지 함수들은 기존과 동일) ...
|
void RetreatFromPlayer() // 함수를 선언할거에요 -> 플레이어로부터 도망가는 RetreatFromPlayer를
|
||||||
void RetreatFromPlayer()
|
|
||||||
{
|
{
|
||||||
if (agent.pathPending || (agent.remainingDistance > 0.5f && agent.velocity.magnitude > 0.1f)) { UpdateMovementAnimation(); return; }
|
if (agent.pathPending || (agent.remainingDistance > 0.5f && agent.velocity.magnitude > 0.1f)) { UpdateMovementAnimation(); return; } // 조건이 맞으면 중단할거에요 -> 이미 이동 중이라면
|
||||||
Vector3 dir = (transform.position - playerTransform.position).normalized;
|
Vector3 dir = (transform.position - playerTransform.position).normalized; // 벡터를 계산할거에요 -> 플레이어 반대 방향을
|
||||||
Vector3 pos = transform.position + dir * fleeDistance;
|
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); } }
|
if (NavMesh.SamplePosition(pos, out NavMeshHit hit, 5f, NavMesh.AllAreas)) { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(hit.position); } } // 조건이 맞으면 실행할거에요 -> 유효한 위치라면 그곳으로 이동하기를
|
||||||
UpdateMovementAnimation();
|
UpdateMovementAnimation(); // 함수를 실행할거에요 -> 이동 애니메이션 갱신을
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerator ReloadRoutine()
|
IEnumerator ReloadRoutine() // 코루틴 함수를 선언할거에요 -> 무기 장전(재생성)을 처리하는 ReloadRoutine을
|
||||||
{
|
{
|
||||||
isReloading = true;
|
isReloading = true; // 상태를 바꿀거에요 -> 장전 중 상태를 참으로
|
||||||
yield return new WaitForSeconds(reloadTime);
|
yield return new WaitForSeconds(reloadTime); // 기다릴거에요 -> 장전 시간만큼
|
||||||
if (handModel != null) handModel.SetActive(true);
|
if (handModel != null) handModel.SetActive(true); // 조건이 맞으면 실행할거에요 -> 손에 있는 무기를 다시 보이게
|
||||||
isReloading = false;
|
isReloading = false; // 상태를 바꿀거에요 -> 장전 중 상태를 거짓으로
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnAttackEnd()
|
public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 처리를 하는 OnAttackEnd를
|
||||||
{
|
{
|
||||||
isAttacking = false;
|
isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓으로
|
||||||
if (animator != null) animator.Play(Monster_Idle);
|
if (animator != null) animator.Play(Monster_Idle); // 조건이 맞으면 실행할거에요 -> 대기 애니메이션으로 복귀를
|
||||||
if (!isDead && !isHit) StartCoroutine(RestAfterAttack());
|
if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 조건이 맞으면 실행할거에요 -> 살아있다면 휴식 코루틴 시작을
|
||||||
}
|
}
|
||||||
|
|
||||||
void Patrol()
|
void Patrol() // 함수를 선언할거에요 -> 순찰 로직 Patrol을
|
||||||
{
|
{
|
||||||
if (Time.time < nextPatrolTime) { if (agent.velocity.magnitude < 0.1f) animator.Play(Monster_Idle); return; }
|
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));
|
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); } }
|
if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(hit.position); } } // 조건이 맞으면 실행할거에요 -> 유효한 위치라면 이동하기를
|
||||||
nextPatrolTime = Time.time + patrolInterval;
|
nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을
|
||||||
}
|
}
|
||||||
|
|
||||||
void UpdateMovementAnimation()
|
void UpdateMovementAnimation() // 함수를 선언할거에요 -> 이동 애니메이션 갱신 함수를
|
||||||
{
|
{
|
||||||
if (isAttacking || isHit || isResting) return;
|
if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면
|
||||||
if (agent.velocity.magnitude > 0.1f) animator.Play(walkAnim); else animator.Play(Monster_Idle);
|
if (agent.velocity.magnitude > 0.1f) animator.Play(walkAnim); else animator.Play(Monster_Idle); // 조건에 따라 재생할거에요 -> 걷기 또는 대기 애니메이션을
|
||||||
}
|
}
|
||||||
|
|
||||||
// private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; }
|
|
||||||
//private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; }
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 만능 투사체 (화살, 마법, 돌맹이 공용)
|
/// 만능 투사체 (화살, 마법, 돌맹이 공용)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Projectile : MonoBehaviour
|
public class Projectile : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 Projectile을
|
||||||
{
|
{
|
||||||
[Header("기본 설정")]
|
[Header("기본 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 기본 설정 을
|
||||||
[SerializeField] private float lifetime = 5f; // 수명 (5초 뒤 삭제)
|
[SerializeField] private float lifetime = 5f; // 변수를 선언할거에요 -> 수명(5.0초)을 lifetime에
|
||||||
[SerializeField] private GameObject hitEffectPrefab; // 맞으면 나오는 이펙트 (폭발 등)
|
[SerializeField] private GameObject hitEffectPrefab; // 변수를 선언할거에요 -> 충돌 이펙트 프리팹을 hitEffectPrefab에
|
||||||
[SerializeField] private bool destroyOnHit = true; // 맞으면 사라짐? (관통 아니면 true)
|
[SerializeField] private bool destroyOnHit = true; // 변수를 선언할거에요 -> 충돌 시 삭제 여부를 destroyOnHit에
|
||||||
|
|
||||||
// 내부 변수
|
// 내부 변수
|
||||||
private Vector3 _direction;
|
private Vector3 _direction; // 변수를 선언할거에요 -> 날아갈 방향을 _direction에
|
||||||
private float _speed;
|
private float _speed; // 변수를 선언할거에요 -> 이동 속도를 _speed에
|
||||||
private float _damage;
|
private float _damage; // 변수를 선언할거에요 -> 공격 데미지를 _damage에
|
||||||
private bool _isInitialized = false;
|
private bool _isInitialized = false; // 변수를 초기화할거에요 -> 초기화 여부를 거짓(false)으로
|
||||||
private Rigidbody _rigidbody;
|
private Rigidbody _rigidbody; // 변수를 선언할거에요 -> 물리 컴포넌트 _rigidbody를
|
||||||
|
|
||||||
private void Awake()
|
private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 호출되는 Awake를
|
||||||
{
|
{
|
||||||
_rigidbody = GetComponent<Rigidbody>();
|
_rigidbody = GetComponent<Rigidbody>(); // 컴포넌트를 가져올거에요 -> 리지드바디를
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -28,79 +28,79 @@ public class Projectile : MonoBehaviour
|
||||||
/// <param name="direction">날아갈 방향</param>
|
/// <param name="direction">날아갈 방향</param>
|
||||||
/// <param name="speed">속도 (0이면 물리력으로 날아감)</param>
|
/// <param name="speed">속도 (0이면 물리력으로 날아감)</param>
|
||||||
/// <param name="damage">줄 데미지</param>
|
/// <param name="damage">줄 데미지</param>
|
||||||
public void Initialize(Vector3 direction, float speed, float damage)
|
public void Initialize(Vector3 direction, float speed, float damage) // 함수를 선언할거에요 -> 투사체 정보를 설정하는 Initialize를
|
||||||
{
|
{
|
||||||
_direction = direction.normalized;
|
_direction = direction.normalized; // 값을 저장할거에요 -> 방향 벡터를 정규화해서
|
||||||
_speed = speed;
|
_speed = speed; // 값을 저장할거에요 -> 속도를
|
||||||
_damage = damage;
|
_damage = damage; // 값을 저장할거에요 -> 데미지를
|
||||||
_isInitialized = true;
|
_isInitialized = true; // 상태를 바꿀거에요 -> 초기화 완료 상태를 참으로
|
||||||
|
|
||||||
// 속도가 있다? -> 화살/마법 (직선 운동)
|
// 속도가 있다? -> 화살/마법 (직선 운동)
|
||||||
if (_rigidbody != null && speed > 0)
|
if (_rigidbody != null && speed > 0) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있고 속도가 있다면
|
||||||
{
|
{
|
||||||
_rigidbody.velocity = _direction * _speed;
|
_rigidbody.velocity = _direction * _speed; // 값을 넣을거에요 -> 속도 벡터를 리지드바디 속도에
|
||||||
_rigidbody.useGravity = false; // 마법은 보통 중력 무시
|
_rigidbody.useGravity = false; // 설정을 바꿀거에요 -> 중력 영향을 끄기로 (직선 비행)
|
||||||
}
|
}
|
||||||
// 속도가 0이다? -> 돌맹이 (외부에서 물리력 가함)
|
// 속도가 0이다? -> 돌맹이 (외부에서 물리력 가함)
|
||||||
else if (_rigidbody != null && speed == 0)
|
else if (_rigidbody != null && speed == 0) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있고 속도가 0이라면
|
||||||
{
|
{
|
||||||
_rigidbody.useGravity = true; // 돌은 중력 받아야 함
|
_rigidbody.useGravity = true; // 설정을 바꿀거에요 -> 중력 영향을 켜기로 (포물선 비행)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 수명 지나면 자동 삭제
|
// 수명 지나면 자동 삭제
|
||||||
Destroy(gameObject, lifetime);
|
Destroy(gameObject, lifetime); // 파괴할거에요 -> 이 오브젝트를 수명 시간이 지나면
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FixedUpdate()
|
private void FixedUpdate() // 함수를 실행할거에요 -> 물리 연산 주기로 호출되는 FixedUpdate를
|
||||||
{
|
{
|
||||||
if (!_isInitialized) return;
|
if (!_isInitialized) return; // 조건이 맞으면 중단할거에요 -> 초기화되지 않았다면
|
||||||
|
|
||||||
// 리지드바디가 없거나, Kinematic인 경우 (강제 이동 보정)
|
// 리지드바디가 없거나, Kinematic인 경우 (강제 이동 보정)
|
||||||
if (_rigidbody == null || _rigidbody.isKinematic)
|
if (_rigidbody == null || _rigidbody.isKinematic) // 조건이 맞으면 실행할거에요 -> 물리 이동을 안 쓰는 상태라면
|
||||||
{
|
{
|
||||||
if (_speed > 0)
|
if (_speed > 0) // 조건이 맞으면 실행할거에요 -> 속도가 설정되어 있다면
|
||||||
{
|
{
|
||||||
transform.position += _direction * _speed * Time.fixedDeltaTime;
|
transform.position += _direction * _speed * Time.fixedDeltaTime; // 이동시킬거에요 -> 방향과 속도에 맞춰 직접 위치를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTriggerEnter(Collider other)
|
private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 충돌이 발생했을 때 OnTriggerEnter를
|
||||||
{
|
{
|
||||||
// 1. 적(몬스터)끼리 맞으면 무시
|
// 1. 적(몬스터)끼리 맞으면 무시
|
||||||
if (other.CompareTag("Enemy") || other.gameObject == gameObject) return;
|
if (other.CompareTag("Enemy") || other.gameObject == gameObject) return; // 조건이 맞으면 중단할거에요 -> 적끼리 충돌했거나 자기 자신이라면
|
||||||
|
|
||||||
// 2. 투사체끼리 충돌 무시
|
// 2. 투사체끼리 충돌 무시
|
||||||
if (other.GetComponent<Projectile>()) return;
|
if (other.GetComponent<Projectile>()) return; // 조건이 맞으면 중단할거에요 -> 다른 투사체와 충돌했다면
|
||||||
|
|
||||||
// 3. 플레이어 피격 처리
|
// 3. 플레이어 피격 처리
|
||||||
if (other.CompareTag("Player"))
|
if (other.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 충돌 대상이 플레이어라면
|
||||||
{
|
{
|
||||||
// PlayerHealth 혹은 IDamageable 스크립트 찾기
|
// PlayerHealth 혹은 IDamageable 스크립트 찾기
|
||||||
var playerHealth = other.GetComponent<PlayerHealth>();
|
var playerHealth = other.GetComponent<PlayerHealth>(); // 컴포넌트를 가져올거에요 -> 플레이어 체력 스크립트를
|
||||||
|
|
||||||
if (playerHealth != null)
|
if (playerHealth != null) // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있다면
|
||||||
{
|
{
|
||||||
// 무적 상태 체크 (있다면)
|
// 무적 상태 체크 (있다면)
|
||||||
if (!playerHealth.isInvincible)
|
if (!playerHealth.isInvincible) // 조건이 맞으면 실행할거에요 -> 플레이어가 무적 상태가 아니라면
|
||||||
{
|
{
|
||||||
playerHealth.TakeDamage(_damage);
|
playerHealth.TakeDamage(_damage); // 함수를 실행할거에요 -> 데미지를 입히는 TakeDamage를
|
||||||
Debug.Log($"🎯 [적중] 플레이어에게 {_damage} 데미지!");
|
Debug.Log($"🎯 [적중] 플레이어에게 {_damage} 데미지!"); // 로그를 출력할거에요 -> 적중 메시지를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 벽이나 땅에 닿았을 때도 이펙트
|
// 4. 벽이나 땅에 닿았을 때도 이펙트
|
||||||
if (other.CompareTag("Ground") || other.CompareTag("Wall") || other.CompareTag("Player"))
|
if (other.CompareTag("Ground") || other.CompareTag("Wall") || other.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 땅, 벽, 플레이어에 닿았다면
|
||||||
{
|
{
|
||||||
if (hitEffectPrefab != null)
|
if (hitEffectPrefab != null) // 조건이 맞으면 실행할거에요 -> 충돌 이펙트 프리팹이 있다면
|
||||||
{
|
{
|
||||||
Instantiate(hitEffectPrefab, transform.position, Quaternion.identity);
|
Instantiate(hitEffectPrefab, transform.position, Quaternion.identity); // 생성할거에요 -> 이펙트를 충돌 위치에
|
||||||
}
|
}
|
||||||
|
|
||||||
if (destroyOnHit)
|
if (destroyOnHit) // 조건이 맞으면 실행할거에요 -> 충돌 시 삭제 옵션이 켜져 있다면
|
||||||
{
|
{
|
||||||
Destroy(gameObject);
|
Destroy(gameObject); // 파괴할거에요 -> 이 투사체 오브젝트를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,75 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 보스 카운터 시스템의 임계치, 가중치, 감소(Decay) 설정을 담는 ScriptableObject.
|
/// 보스 카운터 시스템의 임계치, 가중치, 감소(Decay) 설정을 담는 ScriptableObject.
|
||||||
/// Inspector에서 밸런싱을 위해 쉽게 조정할 수 있습니다.
|
/// Inspector에서 밸런싱을 위해 쉽게 조정할 수 있습니다.
|
||||||
///
|
|
||||||
/// [사용법] Project 창에서 우클릭 → Create → Boss/Counter Config
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[CreateAssetMenu(fileName = "BossCounterConfig", menuName = "Boss/Counter Config")]
|
[CreateAssetMenu(fileName = "BossCounterConfig", menuName = "Boss/Counter Config")] // 메뉴를 만들거에요 -> 에셋 생성 메뉴에 "Boss/Counter Config"를
|
||||||
public class BossCounterConfig : ScriptableObject
|
public class BossCounterConfig : ScriptableObject // 클래스를 선언할거에요 -> ScriptableObject를 상속받는 BossCounterConfig를
|
||||||
{
|
{
|
||||||
[Header("══ 기본 임계치 (첫 런 / 잠금 해제 전) ══")]
|
[Header("══ 기본 임계치 (첫 런 / 잠금 해제 전) ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 기본 임계치 (첫 런 / 잠금 해제 전) ══ 을
|
||||||
[Tooltip("회피 카운터 발동 조건: 10초 내 회피 횟수")]
|
[Tooltip("회피 카운터 발동 조건: 10초 내 회피 횟수")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
public int dodgeThreshold = 5;
|
public int dodgeThreshold = 5; // 변수를 선언할거에요 -> 회피 횟수 임계값(5)을 dodgeThreshold에
|
||||||
|
|
||||||
[Tooltip("조준 카운터 발동 조건: 10초 내 조준 유지 시간(초)")]
|
[Tooltip("조준 카운터 발동 조건: 10초 내 조준 유지 시간(초)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
public float aimThreshold = 4.0f;
|
public float aimThreshold = 4.0f; // 변수를 선언할거에요 -> 조준 시간 임계값(4.0초)을 aimThreshold에
|
||||||
|
|
||||||
[Tooltip("관통 카운터 발동 조건: 10초 내 관통 비율")]
|
[Tooltip("관통 카운터 발동 조건: 10초 내 관통 비율")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[Range(0f, 1f)]
|
[Range(0f, 1f)] // 슬라이더를 표시할거에요 -> 0부터 1 사이로
|
||||||
public float pierceThreshold = 0.6f;
|
public float pierceThreshold = 0.6f; // 변수를 선언할거에요 -> 관통 비율 임계값(60%)을 pierceThreshold에
|
||||||
|
|
||||||
[Tooltip("관통 비율 판단을 위한 최소 발사 횟수")]
|
[Tooltip("관통 비율 판단을 위한 최소 발사 횟수")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
public int minShotsForPierceCheck = 3;
|
public int minShotsForPierceCheck = 3; // 변수를 선언할거에요 -> 최소 발사 횟수(3)를 minShotsForPierceCheck에
|
||||||
|
|
||||||
[Header("══ 잠금 해제 후 임계치 감소 ══")]
|
[Header("══ 잠금 해제 후 임계치 감소 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 잠금 해제 후 임계치 감소 ══ 를
|
||||||
[Tooltip("잠금 해제된 카운터의 임계치 감소 비율 (0.8 = 20% 낮아짐)")]
|
[Tooltip("잠금 해제된 카운터의 임계치 감소 비율 (0.8 = 20% 낮아짐)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[Range(0.5f, 1.0f)]
|
[Range(0.5f, 1.0f)] // 슬라이더를 표시할거에요 -> 0.5부터 1.0 사이로
|
||||||
public float unlockedThresholdMultiplier = 0.8f;
|
public float unlockedThresholdMultiplier = 0.8f; // 변수를 선언할거에요 -> 잠금 해제 시 감소 배율(0.8)을 unlockedThresholdMultiplier에
|
||||||
|
|
||||||
[Header("══ 가중치 설정 (확률 가중치 방식) ══")]
|
[Header("══ 가중치 설정 (확률 가중치 방식) ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 가중치 설정 (확률 가중치 방식) ══ 을
|
||||||
[Tooltip("카운터 발동 시 해당 카운터 패턴에 추가되는 가중치")]
|
[Tooltip("카운터 발동 시 해당 카운터 패턴에 추가되는 가중치")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
public float counterWeightBonus = 3f;
|
public float counterWeightBonus = 3f; // 변수를 선언할거에요 -> 카운터 패턴 보너스 가중치(3.0)를 counterWeightBonus에
|
||||||
|
|
||||||
[Tooltip("카운터 발동 시 보조 패턴에 추가되는 가중치")]
|
[Tooltip("카운터 발동 시 보조 패턴에 추가되는 가중치")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
public float counterSubWeightBonus = 2f;
|
public float counterSubWeightBonus = 2f; // 변수를 선언할거에요 -> 보조 패턴 보너스 가중치(2.0)를 counterSubWeightBonus에
|
||||||
|
|
||||||
[Header("══ Decay(감소) 설정 ══")]
|
[Header("══ Decay(감소) 설정 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ Decay(감소) 설정 ══ 을
|
||||||
[Tooltip("카운터 모드 유지 시간(초). 이 시간 동안 조건 미충족 시 비활성화")]
|
[Tooltip("카운터 모드 유지 시간(초). 이 시간 동안 조건 미충족 시 비활성화")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
public float counterDecayTime = 8f;
|
public float counterDecayTime = 8f; // 변수를 선언할거에요 -> 카운터 유지 시간(8.0초)을 counterDecayTime에
|
||||||
|
|
||||||
[Header("══ 빈도 제한 ══")]
|
[Header("══ 빈도 제한 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 빈도 제한 ══ 을
|
||||||
[Tooltip("카운터 패턴 선택 확률 상한 (0.25 = 최대 25%)")]
|
[Tooltip("카운터 패턴 선택 확률 상한 (0.25 = 최대 25%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[Range(0.1f, 0.5f)]
|
[Range(0.1f, 0.5f)] // 슬라이더를 표시할거에요 -> 0.1부터 0.5 사이로
|
||||||
public float maxCounterFrequency = 0.25f;
|
public float maxCounterFrequency = 0.25f; // 변수를 선언할거에요 -> 카운터 최대 빈도(25%)를 maxCounterFrequency에
|
||||||
|
|
||||||
[Tooltip("카운터 패턴 발동 후 쿨타임(초)")]
|
[Tooltip("카운터 패턴 발동 후 쿨타임(초)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
public float counterCooldown = 5f;
|
public float counterCooldown = 5f; // 변수를 선언할거에요 -> 카운터 재사용 대기시간(5.0초)을 counterCooldown에
|
||||||
|
|
||||||
[Header("══ 습관 변경 보상 ══")]
|
[Header("══ 습관 변경 보상 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 습관 변경 보상 ══ 을
|
||||||
[Tooltip("플레이어가 이전 런에서 발동된 카운터 습관을 이번 런에서 안 보이면, 보스 일반 패턴 난이도 감소 비율")]
|
[Tooltip("플레이어가 이전 런에서 발동된 카운터 습관을 이번 런에서 안 보이면, 보스 일반 패턴 난이도 감소 비율")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[Range(0f, 0.3f)]
|
[Range(0f, 0.3f)] // 슬라이더를 표시할거에요 -> 0부터 0.3 사이로
|
||||||
public float habitChangeRewardRatio = 0.15f;
|
public float habitChangeRewardRatio = 0.15f; // 변수를 선언할거에요 -> 습관 변경 보상 비율(15%)을 habitChangeRewardRatio에
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
// 임계치 조회 (잠금 해제 여부 반영)
|
// 임계치 조회 (잠금 해제 여부 반영)
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
/// <summary>현재 유효 임계치 반환 (잠금 해제 시 낮아짐)</summary>
|
/// <summary>현재 유효 임계치 반환 (잠금 해제 시 낮아짐)</summary>
|
||||||
public int GetEffectiveDodgeThreshold(bool isUnlocked)
|
public int GetEffectiveDodgeThreshold(bool isUnlocked) // 함수를 선언할거에요 -> 잠금 해제 여부에 따른 회피 임계값을 반환하는 GetEffectiveDodgeThreshold를
|
||||||
{
|
{
|
||||||
if (!isUnlocked) return dodgeThreshold;
|
if (!isUnlocked) return dodgeThreshold; // 조건이 맞으면 반환할거에요 -> 잠금 상태라면 기본 임계값을
|
||||||
return Mathf.Max(2, Mathf.RoundToInt(dodgeThreshold * unlockedThresholdMultiplier));
|
return Mathf.Max(2, Mathf.RoundToInt(dodgeThreshold * unlockedThresholdMultiplier)); // 값을 계산해서 반환할거에요 -> 기본값에 배율을 곱하고 최소 2 이상이 되도록
|
||||||
}
|
}
|
||||||
|
|
||||||
public float GetEffectiveAimThreshold(bool isUnlocked)
|
public float GetEffectiveAimThreshold(bool isUnlocked) // 함수를 선언할거에요 -> 잠금 해제 여부에 따른 조준 임계값을 반환하는 GetEffectiveAimThreshold를
|
||||||
{
|
{
|
||||||
if (!isUnlocked) return aimThreshold;
|
if (!isUnlocked) return aimThreshold; // 조건이 맞으면 반환할거에요 -> 잠금 상태라면 기본 임계값을
|
||||||
return aimThreshold * unlockedThresholdMultiplier;
|
return aimThreshold * unlockedThresholdMultiplier; // 값을 계산해서 반환할거에요 -> 기본값에 배율을 곱한 값을
|
||||||
}
|
}
|
||||||
|
|
||||||
public float GetEffectivePierceThreshold(bool isUnlocked)
|
public float GetEffectivePierceThreshold(bool isUnlocked) // 함수를 선언할거에요 -> 잠금 해제 여부에 따른 관통 임계값을 반환하는 GetEffectivePierceThreshold를
|
||||||
{
|
{
|
||||||
if (!isUnlocked) return pierceThreshold;
|
if (!isUnlocked) return pierceThreshold; // 조건이 맞으면 반환할거에요 -> 잠금 상태라면 기본 임계값을
|
||||||
return pierceThreshold * unlockedThresholdMultiplier;
|
return pierceThreshold * unlockedThresholdMultiplier; // 값을 계산해서 반환할거에요 -> 기본값에 배율을 곱한 값을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,48 +1,49 @@
|
||||||
using System.Collections;
|
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using UnityEngine.UI;
|
using UnityEngine.UI; // UI 기능을 사용할거에요 -> UnityEngine.UI를
|
||||||
using TMPro;
|
using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 보스 카운터 시스템의 연출/UI를 담당합니다.
|
/// 보스 카운터 시스템의 연출/UI를 담당합니다.
|
||||||
/// 카운터 발동 시 보스 대사, 화면 효과 등을 처리합니다.
|
|
||||||
///
|
|
||||||
/// "...또 그 수법이냐" 같은 대사로 "보스가 기억한다"는 서사 전달.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class BossCounterFeedback : MonoBehaviour
|
public class BossCounterFeedback : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossCounterFeedback을
|
||||||
{
|
{
|
||||||
[Header("참조")]
|
[Header("참조")] // 인스펙터 창에 제목을 표시할거에요 -> 참조 를
|
||||||
[SerializeField] private BossCounterSystem counterSystem;
|
[SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 카운터 시스템 스크립트를 연결할 counterSystem을
|
||||||
|
|
||||||
[Header("UI 요소 (선택)")]
|
[Header("UI 요소 (선택)")] // 인스펙터 창에 제목을 표시할거에요 -> UI 요소 (선택) 을
|
||||||
[SerializeField] private TextMeshProUGUI bossDialogueText;
|
[SerializeField] private TextMeshProUGUI bossDialogueText; // 변수를 선언할거에요 -> 보스 대사를 표시할 텍스트 UI를
|
||||||
[SerializeField] private CanvasGroup dialogueCanvasGroup;
|
[SerializeField] private CanvasGroup dialogueCanvasGroup; // 변수를 선언할거에요 -> 대사창의 투명도를 조절할 캔버스 그룹을
|
||||||
[SerializeField] private float dialogueDisplayDuration = 3f;
|
[SerializeField] private float dialogueDisplayDuration = 3f; // 변수를 선언할거에요 -> 대사 표시 시간(3초)을 dialogueDisplayDuration에
|
||||||
[SerializeField] private float dialogueFadeDuration = 0.5f;
|
[SerializeField] private float dialogueFadeDuration = 0.5f; // 변수를 선언할거에요 -> 대사 페이드 시간(0.5초)을 dialogueFadeDuration에
|
||||||
|
|
||||||
[Header("카운터별 보스 대사")]
|
[Header("카운터별 보스 대사")] // 인스펙터 창에 제목을 표시할거에요 -> 카운터별 보스 대사 를
|
||||||
[SerializeField] private string[] dodgeCounterDialogues = new string[]
|
[SerializeField]
|
||||||
|
private string[] dodgeCounterDialogues = new string[] // 배열을 초기화할거에요 -> 회피 카운터 대사 목록을
|
||||||
{
|
{
|
||||||
"...또 도망치려는 건가.",
|
"...또 도망치려는 건가.",
|
||||||
"네 발은 이미 읽었다.",
|
"네 발은 이미 읽었다.",
|
||||||
"아무리 피해도 소용없어."
|
"아무리 피해도 소용없어."
|
||||||
};
|
};
|
||||||
|
|
||||||
[SerializeField] private string[] aimCounterDialogues = new string[]
|
[SerializeField]
|
||||||
|
private string[] aimCounterDialogues = new string[] // 배열을 초기화할거에요 -> 조준 카운터 대사 목록을
|
||||||
{
|
{
|
||||||
"그렇게 오래 노려봐야...",
|
"그렇게 오래 노려봐야...",
|
||||||
"느린 조준은 빈틈이지.",
|
"느린 조준은 빈틈이지.",
|
||||||
"시간을 줄 생각은 없다."
|
"시간을 줄 생각은 없다."
|
||||||
};
|
};
|
||||||
|
|
||||||
[SerializeField] private string[] pierceCounterDialogues = new string[]
|
[SerializeField]
|
||||||
|
private string[] pierceCounterDialogues = new string[] // 배열을 초기화할거에요 -> 관통 카운터 대사 목록을
|
||||||
{
|
{
|
||||||
"그 화살은 더 이상 통하지 않아.",
|
"그 화살은 더 이상 통하지 않아.",
|
||||||
"관통? 이번엔 막아주지.",
|
"관통? 이번엔 막아주지.",
|
||||||
"같은 수를 반복하다니."
|
"같은 수를 반복하다니."
|
||||||
};
|
};
|
||||||
|
|
||||||
[SerializeField] private string[] habitChangeDialogues = new string[]
|
[SerializeField]
|
||||||
|
private string[] habitChangeDialogues = new string[] // 배열을 초기화할거에요 -> 습관 변경 보상 대사 목록을
|
||||||
{
|
{
|
||||||
"...다른 수를 쓰는 건가.",
|
"...다른 수를 쓰는 건가.",
|
||||||
"흥, 조금은 배운 모양이군.",
|
"흥, 조금은 배운 모양이군.",
|
||||||
|
|
@ -50,33 +51,34 @@ public class BossCounterFeedback : MonoBehaviour
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── 잠금 해제 여부에 따른 첫 발동 대사 ──
|
// ── 잠금 해제 여부에 따른 첫 발동 대사 ──
|
||||||
[Header("첫 잠금 해제 시 대사 (서사 강조)")]
|
[Header("첫 잠금 해제 시 대사 (서사 강조)")] // 인스펙터 창에 제목을 표시할거에요 -> 첫 잠금 해제 시 대사 (서사 강조) 를
|
||||||
[SerializeField] private string[] firstUnlockDialogues = new string[]
|
[SerializeField]
|
||||||
|
private string[] firstUnlockDialogues = new string[] // 배열을 초기화할거에요 -> 첫 해금 시 출력할 대사 목록을
|
||||||
{
|
{
|
||||||
"...기억했다. 네 버릇을.",
|
"...기억했다. 네 버릇을.",
|
||||||
"이제 알겠어. 네 전투 방식이.",
|
"이제 알겠어. 네 전투 방식이.",
|
||||||
"한 번이면 충분해. 패턴을 읽었다."
|
"한 번이면 충분해. 패턴을 읽었다."
|
||||||
};
|
};
|
||||||
|
|
||||||
private Coroutine dialogueCoroutine;
|
private Coroutine dialogueCoroutine; // 변수를 선언할거에요 -> 실행 중인 대사 코루틴을 저장할 dialogueCoroutine을
|
||||||
|
|
||||||
private void OnEnable()
|
private void OnEnable() // 함수를 실행할거에요 -> 오브젝트 활성화 시 호출되는 OnEnable을
|
||||||
{
|
{
|
||||||
if (counterSystem != null)
|
if (counterSystem != null) // 조건이 맞으면 실행할거에요 -> 카운터 시스템이 연결되어 있다면
|
||||||
{
|
{
|
||||||
counterSystem.OnCounterActivated.AddListener(OnCounterActivated);
|
counterSystem.OnCounterActivated.AddListener(OnCounterActivated); // 구독할거에요 -> 카운터 활성 이벤트에 OnCounterActivated를
|
||||||
counterSystem.OnCounterDeactivated.AddListener(OnCounterDeactivated);
|
counterSystem.OnCounterDeactivated.AddListener(OnCounterDeactivated); // 구독할거에요 -> 카운터 비활성 이벤트에 OnCounterDeactivated를
|
||||||
counterSystem.OnHabitChangeRewarded.AddListener(OnHabitChangeRewarded);
|
counterSystem.OnHabitChangeRewarded.AddListener(OnHabitChangeRewarded); // 구독할거에요 -> 보상 이벤트에 OnHabitChangeRewarded를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDisable()
|
private void OnDisable() // 함수를 실행할거에요 -> 오브젝트 비활성화 시 호출되는 OnDisable을
|
||||||
{
|
{
|
||||||
if (counterSystem != null)
|
if (counterSystem != null) // 조건이 맞으면 실행할거에요 -> 카운터 시스템이 연결되어 있다면
|
||||||
{
|
{
|
||||||
counterSystem.OnCounterActivated.RemoveListener(OnCounterActivated);
|
counterSystem.OnCounterActivated.RemoveListener(OnCounterActivated); // 구독 해제할거에요 -> 카운터 활성 이벤트를
|
||||||
counterSystem.OnCounterDeactivated.RemoveListener(OnCounterDeactivated);
|
counterSystem.OnCounterDeactivated.RemoveListener(OnCounterDeactivated); // 구독 해제할거에요 -> 카운터 비활성 이벤트를
|
||||||
counterSystem.OnHabitChangeRewarded.RemoveListener(OnHabitChangeRewarded);
|
counterSystem.OnHabitChangeRewarded.RemoveListener(OnHabitChangeRewarded); // 구독 해제할거에요 -> 보상 이벤트를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,106 +86,100 @@ public class BossCounterFeedback : MonoBehaviour
|
||||||
// 이벤트 핸들러
|
// 이벤트 핸들러
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
private void OnCounterActivated(CounterType type)
|
private void OnCounterActivated(CounterType type) // 함수를 선언할거에요 -> 카운터 활성 시 호출될 OnCounterActivated를
|
||||||
{
|
{
|
||||||
var persistence = BossCounterPersistence.Instance;
|
var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소 인스턴스를 persistence에
|
||||||
|
|
||||||
// 첫 잠금 해제인지 확인 (총 발동 횟수가 1이면 방금 처음 잠금 해제된 것)
|
// 첫 잠금 해제인지 확인 (총 발동 횟수가 1이면 방금 처음 잠금 해제된 것)
|
||||||
bool isFirstUnlock = false;
|
bool isFirstUnlock = false; // 변수를 초기화할거에요 -> 첫 해금 여부를 거짓(false)으로
|
||||||
if (persistence != null)
|
if (persistence != null) // 조건이 맞으면 실행할거에요 -> 저장소가 있다면
|
||||||
{
|
{
|
||||||
int activations = type switch
|
int activations = type switch // 값을 가져올거에요 -> 타입에 따른 발동 횟수를
|
||||||
{
|
{
|
||||||
CounterType.Dodge => persistence.Data.dodgeCounterActivations,
|
CounterType.Dodge => persistence.Data.dodgeCounterActivations,
|
||||||
CounterType.Aim => persistence.Data.aimCounterActivations,
|
CounterType.Aim => persistence.Data.aimCounterActivations,
|
||||||
CounterType.Pierce => persistence.Data.pierceCounterActivations,
|
CounterType.Pierce => persistence.Data.pierceCounterActivations,
|
||||||
_ => 0
|
_ => 0
|
||||||
};
|
};
|
||||||
isFirstUnlock = (activations <= 1);
|
isFirstUnlock = (activations <= 1); // 판단할거에요 -> 발동 횟수가 1 이하라면 첫 해금이라고
|
||||||
}
|
}
|
||||||
|
|
||||||
// 대사 선택
|
// 대사 선택
|
||||||
string dialogue;
|
string dialogue; // 변수를 선언할거에요 -> 출력할 대사를 담을 dialogue를
|
||||||
if (isFirstUnlock)
|
if (isFirstUnlock) // 조건이 맞으면 실행할거에요 -> 첫 해금이라면
|
||||||
{
|
{
|
||||||
dialogue = firstUnlockDialogues[Random.Range(0, firstUnlockDialogues.Length)];
|
dialogue = firstUnlockDialogues[Random.Range(0, firstUnlockDialogues.Length)]; // 선택할거에요 -> 첫 해금 대사 중 하나를 랜덤으로
|
||||||
}
|
}
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 이미 해금된 상태라면
|
||||||
{
|
{
|
||||||
dialogue = type switch
|
dialogue = type switch // 분기할거에요 -> 카운터 타입에 따라
|
||||||
{
|
{
|
||||||
CounterType.Dodge => dodgeCounterDialogues[Random.Range(0, dodgeCounterDialogues.Length)],
|
CounterType.Dodge => dodgeCounterDialogues[Random.Range(0, dodgeCounterDialogues.Length)], // 선택할거에요 -> 회피 대사를
|
||||||
CounterType.Aim => aimCounterDialogues[Random.Range(0, aimCounterDialogues.Length)],
|
CounterType.Aim => aimCounterDialogues[Random.Range(0, aimCounterDialogues.Length)], // 선택할거에요 -> 조준 대사를
|
||||||
CounterType.Pierce => pierceCounterDialogues[Random.Range(0, pierceCounterDialogues.Length)],
|
CounterType.Pierce => pierceCounterDialogues[Random.Range(0, pierceCounterDialogues.Length)], // 선택할거에요 -> 관통 대사를
|
||||||
_ => ""
|
_ => "" // 기본값은 빈 문자열로
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ShowDialogue(dialogue);
|
ShowDialogue(dialogue); // 함수를 실행할거에요 -> 선택된 대사를 화면에 띄우는 ShowDialogue를
|
||||||
|
|
||||||
// TODO: 여기에 추가 연출 넣기
|
Debug.Log($"[BossCounterFeedback] {type} 카운터 연출 재생" + (isFirstUnlock ? " (첫 잠금 해제!)" : "")); // 로그를 출력할거에요 -> 연출 재생 알림을
|
||||||
// - 보스 전용 애니메이션 트리거 (예: animator.SetTrigger("CounterReady"))
|
|
||||||
// - 보스 주변 이펙트 (파티클, 오라 등)
|
|
||||||
// - 카메라 연출 (줌인, 슬로모션 등)
|
|
||||||
// - 사운드 효과
|
|
||||||
Debug.Log($"[BossCounterFeedback] {type} 카운터 연출 재생" +
|
|
||||||
(isFirstUnlock ? " (첫 잠금 해제!)" : ""));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCounterDeactivated(CounterType type)
|
private void OnCounterDeactivated(CounterType type) // 함수를 선언할거에요 -> 카운터 종료 시 호출될 OnCounterDeactivated를
|
||||||
{
|
{
|
||||||
// 카운터 해제 시 연출 (이펙트 종료 등)
|
// 카운터 해제 시 연출 (이펙트 종료 등)
|
||||||
Debug.Log($"[BossCounterFeedback] {type} 카운터 연출 종료");
|
Debug.Log($"[BossCounterFeedback] {type} 카운터 연출 종료"); // 로그를 출력할거에요 -> 연출 종료 알림을
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnHabitChangeRewarded()
|
private void OnHabitChangeRewarded() // 함수를 선언할거에요 -> 보상 지급 시 호출될 OnHabitChangeRewarded를
|
||||||
{
|
{
|
||||||
string dialogue = habitChangeDialogues[Random.Range(0, habitChangeDialogues.Length)];
|
string dialogue = habitChangeDialogues[Random.Range(0, habitChangeDialogues.Length)]; // 선택할거에요 -> 보상 대사 중 하나를 랜덤으로
|
||||||
ShowDialogue(dialogue);
|
ShowDialogue(dialogue); // 함수를 실행할거에요 -> 대사를 띄우는 ShowDialogue를
|
||||||
|
|
||||||
Debug.Log("[BossCounterFeedback] 습관 변경 보상 연출!");
|
Debug.Log("[BossCounterFeedback] 습관 변경 보상 연출!"); // 로그를 출력할거에요 -> 보상 연출 알림을
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
// 대사 표시
|
// 대사 표시
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
private void ShowDialogue(string text)
|
private void ShowDialogue(string text) // 함수를 선언할거에요 -> 텍스트를 UI에 표시하는 ShowDialogue를
|
||||||
{
|
{
|
||||||
if (bossDialogueText == null || dialogueCanvasGroup == null) return;
|
if (bossDialogueText == null || dialogueCanvasGroup == null) return; // 조건이 맞으면 중단할거에요 -> UI 요소가 연결되지 않았다면
|
||||||
|
|
||||||
if (dialogueCoroutine != null)
|
if (dialogueCoroutine != null) // 조건이 맞으면 실행할거에요 -> 이미 실행 중인 대사가 있다면
|
||||||
StopCoroutine(dialogueCoroutine);
|
StopCoroutine(dialogueCoroutine); // 중단할거에요 -> 기존 코루틴을
|
||||||
|
|
||||||
dialogueCoroutine = StartCoroutine(DialogueRoutine(text));
|
dialogueCoroutine = StartCoroutine(DialogueRoutine(text)); // 코루틴을 시작할거에요 -> 새 대사를 출력하는 DialogueRoutine을
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerator DialogueRoutine(string text)
|
private IEnumerator DialogueRoutine(string text) // 코루틴 함수를 선언할거에요 -> 페이드 효과와 함께 대사를 출력하는 DialogueRoutine을
|
||||||
{
|
{
|
||||||
bossDialogueText.text = text;
|
bossDialogueText.text = text; // 값을 설정할거에요 -> UI 텍스트 내용을 전달받은 text로
|
||||||
dialogueCanvasGroup.alpha = 0f;
|
dialogueCanvasGroup.alpha = 0f; // 값을 설정할거에요 -> 투명도를 0(안 보임)으로
|
||||||
|
|
||||||
// 페이드 인
|
// 페이드 인
|
||||||
float t = 0f;
|
float t = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로
|
||||||
while (t < dialogueFadeDuration)
|
while (t < dialogueFadeDuration) // 반복할거에요 -> 경과 시간이 페이드 시간보다 작을 동안
|
||||||
{
|
{
|
||||||
t += Time.deltaTime;
|
t += Time.deltaTime; // 값을 더할거에요 -> 경과 시간에 프레임 시간을
|
||||||
dialogueCanvasGroup.alpha = t / dialogueFadeDuration;
|
dialogueCanvasGroup.alpha = t / dialogueFadeDuration; // 값을 조절할거에요 -> 투명도를 서서히 1로
|
||||||
yield return null;
|
yield return null; // 대기할거에요 -> 다음 프레임까지
|
||||||
}
|
}
|
||||||
dialogueCanvasGroup.alpha = 1f;
|
dialogueCanvasGroup.alpha = 1f; // 값을 확정할거에요 -> 투명도를 완전 불투명(1)으로
|
||||||
|
|
||||||
// 유지
|
// 유지
|
||||||
yield return new WaitForSeconds(dialogueDisplayDuration);
|
yield return new WaitForSeconds(dialogueDisplayDuration); // 기다릴거에요 -> 대사 유지 시간만큼
|
||||||
|
|
||||||
// 페이드 아웃
|
// 페이드 아웃
|
||||||
t = 0f;
|
t = 0f; // 변수를 초기화할거에요 -> 경과 시간을 다시 0으로
|
||||||
while (t < dialogueFadeDuration)
|
while (t < dialogueFadeDuration) // 반복할거에요 -> 경과 시간이 페이드 시간보다 작을 동안
|
||||||
{
|
{
|
||||||
t += Time.deltaTime;
|
t += Time.deltaTime; // 값을 더할거에요 -> 경과 시간에 프레임 시간을
|
||||||
dialogueCanvasGroup.alpha = 1f - (t / dialogueFadeDuration);
|
dialogueCanvasGroup.alpha = 1f - (t / dialogueFadeDuration); // 값을 조절할거에요 -> 투명도를 서서히 0으로
|
||||||
yield return null;
|
yield return null; // 대기할거에요 -> 다음 프레임까지
|
||||||
}
|
}
|
||||||
dialogueCanvasGroup.alpha = 0f;
|
dialogueCanvasGroup.alpha = 0f; // 값을 확정할거에요 -> 투명도를 완전 투명(0)으로
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,50 +1,40 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 런 간 영구 저장되는 보스 카운터 잠금 해제 데이터.
|
/// 런 간 영구 저장되는 보스 카운터 잠금 해제 데이터.
|
||||||
/// "보스가 플레이어를 기억한다"는 서사를 담당합니다.
|
|
||||||
///
|
|
||||||
/// 핵심 규칙:
|
|
||||||
/// - 잠금 해제는 영구 (런이 끝나도 유지)
|
|
||||||
/// - 발동은 조건부 (해당 런에서 플레이어가 습관을 보여야만 활성화)
|
|
||||||
/// - 잠금 해제된 카운터는 임계치가 약간 낮아짐 (보스가 "이미 알고 있으니 더 빨리 눈치챔")
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[System.Serializable]
|
[System.Serializable] // 직렬화할거에요 -> 인스펙터나 JSON으로 저장 가능하게
|
||||||
public class BossCounterSaveData
|
public class BossCounterSaveData // 클래스를 선언할거에요 -> 저장할 데이터 구조체인 BossCounterSaveData를
|
||||||
{
|
{
|
||||||
public bool dodgeCounterUnlocked = false;
|
public bool dodgeCounterUnlocked = false; // 변수를 선언할거에요 -> 회피 카운터 해금 여부를
|
||||||
public bool aimCounterUnlocked = false;
|
public bool aimCounterUnlocked = false; // 변수를 선언할거에요 -> 조준 카운터 해금 여부를
|
||||||
public bool pierceCounterUnlocked = false;
|
public bool pierceCounterUnlocked = false; // 변수를 선언할거에요 -> 관통 카운터 해금 여부를
|
||||||
|
|
||||||
/// <summary>각 카운터가 발동된 총 횟수 (연출/난이도 스케일링에 활용 가능)</summary>
|
/// <summary>각 카운터가 발동된 총 횟수</summary>
|
||||||
public int dodgeCounterActivations = 0;
|
public int dodgeCounterActivations = 0; // 변수를 선언할거에요 -> 회피 카운터 총 발동 횟수를
|
||||||
public int aimCounterActivations = 0;
|
public int aimCounterActivations = 0; // 변수를 선언할거에요 -> 조준 카운터 총 발동 횟수를
|
||||||
public int pierceCounterActivations = 0;
|
public int pierceCounterActivations = 0; // 변수를 선언할거에요 -> 관통 카운터 총 발동 횟수를
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public class BossCounterPersistence : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossCounterPersistence를
|
||||||
/// 보스 카운터 영구 데이터를 관리합니다.
|
|
||||||
/// PlayerPrefs 기반으로 런 간 저장/로드합니다.
|
|
||||||
/// </summary>
|
|
||||||
public class BossCounterPersistence : MonoBehaviour
|
|
||||||
{
|
{
|
||||||
public static BossCounterPersistence Instance { get; private set; }
|
public static BossCounterPersistence Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스 접근용 Instance를
|
||||||
|
|
||||||
private const string SAVE_KEY = "BossCounterData";
|
private const string SAVE_KEY = "BossCounterData"; // 상수를 정의할거에요 -> 저장에 사용할 키 이름("BossCounterData")을 SAVE_KEY에
|
||||||
|
|
||||||
public BossCounterSaveData Data { get; private set; }
|
public BossCounterSaveData Data { get; private set; } // 프로퍼티를 선언할거에요 -> 실제 저장 데이터를 담을 Data를
|
||||||
|
|
||||||
private void Awake()
|
private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 호출되는 Awake를
|
||||||
{
|
{
|
||||||
if (Instance != null && Instance != this)
|
if (Instance != null && Instance != this) // 조건이 맞으면 실행할거에요 -> 이미 다른 인스턴스가 존재한다면
|
||||||
{
|
{
|
||||||
Destroy(gameObject);
|
Destroy(gameObject); // 파괴할거에요 -> 중복된 이 오브젝트를
|
||||||
return;
|
return; // 중단할거에요 -> 초기화 로직을
|
||||||
}
|
}
|
||||||
Instance = this;
|
Instance = this; // 값을 저장할거에요 -> 내 자신(this)을 싱글톤 인스턴스에
|
||||||
DontDestroyOnLoad(gameObject);
|
DontDestroyOnLoad(gameObject); // 유지할거에요 -> 씬이 바뀌어도 이 오브젝트를 파괴하지 않게
|
||||||
Load();
|
Load(); // 함수를 실행할거에요 -> 저장된 데이터를 불러오는 Load를
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
@ -52,88 +42,88 @@ public class BossCounterPersistence : MonoBehaviour
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
/// <summary>특정 카운터 타입을 영구 잠금 해제</summary>
|
/// <summary>특정 카운터 타입을 영구 잠금 해제</summary>
|
||||||
public void UnlockCounter(CounterType type)
|
public void UnlockCounter(CounterType type) // 함수를 선언할거에요 -> 카운터를 해금하는 UnlockCounter를
|
||||||
{
|
{
|
||||||
switch (type)
|
switch (type) // 분기할거에요 -> 카운터 타입(type)에 따라
|
||||||
{
|
{
|
||||||
case CounterType.Dodge:
|
case CounterType.Dodge: // 조건이 맞으면 실행할거에요 -> 회피 타입이라면
|
||||||
if (!Data.dodgeCounterUnlocked)
|
if (!Data.dodgeCounterUnlocked) // 조건이 맞으면 실행할거에요 -> 아직 해금되지 않았다면
|
||||||
{
|
{
|
||||||
Data.dodgeCounterUnlocked = true;
|
Data.dodgeCounterUnlocked = true; // 값을 바꿀거에요 -> 회피 해금 상태를 참(true)으로
|
||||||
Debug.Log("[BossCounter] 회피 카운터 영구 잠금 해제!");
|
Debug.Log("[BossCounter] 회피 카운터 영구 잠금 해제!"); // 로그를 출력할거에요 -> 해금 알림 메시지를
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case CounterType.Aim:
|
case CounterType.Aim: // 조건이 맞으면 실행할거에요 -> 조준 타입이라면
|
||||||
if (!Data.aimCounterUnlocked)
|
if (!Data.aimCounterUnlocked) // 조건이 맞으면 실행할거에요 -> 아직 해금되지 않았다면
|
||||||
{
|
{
|
||||||
Data.aimCounterUnlocked = true;
|
Data.aimCounterUnlocked = true; // 값을 바꿀거에요 -> 조준 해금 상태를 참(true)으로
|
||||||
Debug.Log("[BossCounter] 조준 카운터 영구 잠금 해제!");
|
Debug.Log("[BossCounter] 조준 카운터 영구 잠금 해제!"); // 로그를 출력할거에요 -> 해금 알림 메시지를
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case CounterType.Pierce:
|
case CounterType.Pierce: // 조건이 맞으면 실행할거에요 -> 관통 타입이라면
|
||||||
if (!Data.pierceCounterUnlocked)
|
if (!Data.pierceCounterUnlocked) // 조건이 맞으면 실행할거에요 -> 아직 해금되지 않았다면
|
||||||
{
|
{
|
||||||
Data.pierceCounterUnlocked = true;
|
Data.pierceCounterUnlocked = true; // 값을 바꿀거에요 -> 관통 해금 상태를 참(true)으로
|
||||||
Debug.Log("[BossCounter] 관통 카운터 영구 잠금 해제!");
|
Debug.Log("[BossCounter] 관통 카운터 영구 잠금 해제!"); // 로그를 출력할거에요 -> 해금 알림 메시지를
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Save();
|
Save(); // 함수를 실행할거에요 -> 변경된 데이터를 저장하는 Save를
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>카운터가 잠금 해제되어 있는지 확인</summary>
|
/// <summary>카운터가 잠금 해제되어 있는지 확인</summary>
|
||||||
public bool IsUnlocked(CounterType type)
|
public bool IsUnlocked(CounterType type) // 함수를 선언할거에요 -> 해금 여부를 확인하는 IsUnlocked를
|
||||||
{
|
{
|
||||||
return type switch
|
return type switch // 반환할거에요 -> 타입에 따른 해금 변수 값을
|
||||||
{
|
{
|
||||||
CounterType.Dodge => Data.dodgeCounterUnlocked,
|
CounterType.Dodge => Data.dodgeCounterUnlocked, // 매칭되면 반환할거에요 -> 회피 해금 여부를
|
||||||
CounterType.Aim => Data.aimCounterUnlocked,
|
CounterType.Aim => Data.aimCounterUnlocked, // 매칭되면 반환할거에요 -> 조준 해금 여부를
|
||||||
CounterType.Pierce => Data.pierceCounterUnlocked,
|
CounterType.Pierce => Data.pierceCounterUnlocked, // 매칭되면 반환할거에요 -> 관통 해금 여부를
|
||||||
_ => false
|
_ => false // 그 외에는 반환할거에요 -> 거짓(false)을
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>카운터 발동 횟수 기록</summary>
|
/// <summary>카운터 발동 횟수 기록</summary>
|
||||||
public void RecordActivation(CounterType type)
|
public void RecordActivation(CounterType type) // 함수를 선언할거에요 -> 발동 횟수를 기록하는 RecordActivation을
|
||||||
{
|
{
|
||||||
switch (type)
|
switch (type) // 분기할거에요 -> 카운터 타입(type)에 따라
|
||||||
{
|
{
|
||||||
case CounterType.Dodge: Data.dodgeCounterActivations++; break;
|
case CounterType.Dodge: Data.dodgeCounterActivations++; break; // 일치하면 실행할거에요 -> 회피 발동 횟수를 1 증가시키기를
|
||||||
case CounterType.Aim: Data.aimCounterActivations++; break;
|
case CounterType.Aim: Data.aimCounterActivations++; break; // 일치하면 실행할거에요 -> 조준 발동 횟수를 1 증가시키기를
|
||||||
case CounterType.Pierce: Data.pierceCounterActivations++; break;
|
case CounterType.Pierce: Data.pierceCounterActivations++; break; // 일치하면 실행할거에요 -> 관통 발동 횟수를 1 증가시키기를
|
||||||
}
|
}
|
||||||
Save();
|
Save(); // 함수를 실행할거에요 -> 변경된 횟수를 저장하는 Save를
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
// 저장 / 로드 / 리셋
|
// 저장 / 로드 / 리셋
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
public void Save()
|
public void Save() // 함수를 선언할거에요 -> 데이터를 디스크에 저장하는 Save를
|
||||||
{
|
{
|
||||||
string json = JsonUtility.ToJson(Data);
|
string json = JsonUtility.ToJson(Data); // 변환할거에요 -> 데이터 객체를 JSON 문자열로
|
||||||
PlayerPrefs.SetString(SAVE_KEY, json);
|
PlayerPrefs.SetString(SAVE_KEY, json); // 저장할거에요 -> JSON 문자열을 PlayerPrefs에
|
||||||
PlayerPrefs.Save();
|
PlayerPrefs.Save(); // 실행할거에요 -> 변경사항을 디스크에 즉시 쓰기를
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Load()
|
public void Load() // 함수를 선언할거에요 -> 데이터를 불러오는 Load를
|
||||||
{
|
{
|
||||||
if (PlayerPrefs.HasKey(SAVE_KEY))
|
if (PlayerPrefs.HasKey(SAVE_KEY)) // 조건이 맞으면 실행할거에요 -> 저장된 키가 존재한다면
|
||||||
{
|
{
|
||||||
string json = PlayerPrefs.GetString(SAVE_KEY);
|
string json = PlayerPrefs.GetString(SAVE_KEY); // 불러올거에요 -> 저장된 JSON 문자열을
|
||||||
Data = JsonUtility.FromJson<BossCounterSaveData>(json);
|
Data = JsonUtility.FromJson<BossCounterSaveData>(json); // 변환할거에요 -> JSON을 데이터 객체로 복구해서 Data에
|
||||||
}
|
}
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 저장된 데이터가 없다면
|
||||||
{
|
{
|
||||||
Data = new BossCounterSaveData();
|
Data = new BossCounterSaveData(); // 생성할거에요 -> 새로운 빈 데이터 객체를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>모든 영구 데이터 초기화 (디버그/뉴게임+)</summary>
|
/// <summary>모든 영구 데이터 초기화 (디버그/뉴게임+)</summary>
|
||||||
public void ResetAllData()
|
public void ResetAllData() // 함수를 선언할거에요 -> 모든 데이터를 초기화하는 ResetAllData를
|
||||||
{
|
{
|
||||||
Data = new BossCounterSaveData();
|
Data = new BossCounterSaveData(); // 초기화할거에요 -> 데이터 객체를 새 것으로
|
||||||
PlayerPrefs.DeleteKey(SAVE_KEY);
|
PlayerPrefs.DeleteKey(SAVE_KEY); // 삭제할거에요 -> 저장된 키를
|
||||||
Debug.Log("[BossCounter] 모든 보스 기억 데이터 초기화됨");
|
Debug.Log("[BossCounter] 모든 보스 기억 데이터 초기화됨"); // 로그를 출력할거에요 -> 초기화 완료 메시지를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,213 +1,206 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic; // 리스트와 딕셔너리 기능을 사용할거에요 -> System.Collections.Generic을
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using UnityEngine.Events;
|
using UnityEngine.Events; // 유니티 이벤트 기능을 사용할거에요 -> UnityEngine.Events를
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 보스 카운터 시스템 메인 컨트롤러.
|
/// 보스 카운터 시스템 메인 컨트롤러.
|
||||||
/// (수정됨: Config나 Player가 없어도 에러가 나지 않도록 안전장치가 추가된 버전)
|
/// (수정됨: Config나 Player가 없어도 에러가 나지 않도록 안전장치가 추가된 버전)
|
||||||
///
|
|
||||||
/// 핵심 흐름:
|
|
||||||
/// 1. PlayerBehaviorTracker에서 실시간 행동 데이터를 읽음
|
|
||||||
/// 2. 임계치 판단 → 카운터 모드 ON/OFF (잠금 해제 여부에 따라 임계치 다름)
|
|
||||||
/// 3. 가중치 기반으로 보스 패턴 선택
|
|
||||||
/// 4. 첫 발동 시 해당 카운터를 영구 잠금 해제
|
|
||||||
/// 5. 플레이어가 습관을 바꾸면 보상 (난이도 완화)
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class BossCounterSystem : MonoBehaviour
|
public class BossCounterSystem : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossCounterSystem을
|
||||||
{
|
{
|
||||||
[Header("참조")]
|
[Header("참조")] // 인스펙터 창에 제목을 표시할거에요 -> 참조 를
|
||||||
[SerializeField] private BossCounterConfig config;
|
[SerializeField] private BossCounterConfig config; // 변수를 선언할거에요 -> 카운터 설정 파일(BossCounterConfig)을 담을 config를
|
||||||
|
|
||||||
[Header("이벤트 (연출/UI 연동)")]
|
[Header("이벤트 (연출/UI 연동)")] // 인스펙터 창에 제목을 표시할거에요 -> 이벤트 (연출/UI 연동) 을
|
||||||
[Tooltip("카운터 모드가 활성화될 때 발생 (CounterType 전달)")]
|
[Tooltip("카운터 모드가 활성화될 때 발생 (CounterType 전달)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
public UnityEvent<CounterType> OnCounterActivated;
|
public UnityEvent<CounterType> OnCounterActivated; // 이벤트를 선언할거에요 -> 카운터가 켜질 때 알릴 OnCounterActivated를
|
||||||
|
|
||||||
[Tooltip("카운터 모드가 비활성화될 때 발생")]
|
[Tooltip("카운터 모드가 비활성화될 때 발생")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
public UnityEvent<CounterType> OnCounterDeactivated;
|
public UnityEvent<CounterType> OnCounterDeactivated; // 이벤트를 선언할거에요 -> 카운터가 꺼질 때 알릴 OnCounterDeactivated를
|
||||||
|
|
||||||
[Tooltip("보스가 카운터 패턴을 선택했을 때 발생 (패턴 이름 전달)")]
|
[Tooltip("보스가 카운터 패턴을 선택했을 때 발생 (패턴 이름 전달)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
public UnityEvent<string> OnCounterPatternSelected;
|
public UnityEvent<string> OnCounterPatternSelected; // 이벤트를 선언할거에요 -> 패턴이 선택됐을 때 알릴 OnCounterPatternSelected를
|
||||||
|
|
||||||
[Tooltip("플레이어가 습관을 바꿔서 보상받을 때 발생")]
|
[Tooltip("플레이어가 습관을 바꿔서 보상받을 때 발생")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
public UnityEvent OnHabitChangeRewarded;
|
public UnityEvent OnHabitChangeRewarded; // 이벤트를 선언할거에요 -> 습관 변경 보상을 알릴 OnHabitChangeRewarded를
|
||||||
|
|
||||||
// ── 카운터 모드 상태 ──
|
// ── 카운터 모드 상태 ──
|
||||||
private Dictionary<CounterType, bool> activeCounters = new Dictionary<CounterType, bool>()
|
private Dictionary<CounterType, bool> activeCounters = new Dictionary<CounterType, bool>() // 변수를 선언하고 초기화할거에요 -> 각 카운터 타입의 활성 상태를 저장할 딕셔너리 activeCounters를
|
||||||
{
|
{
|
||||||
{ CounterType.Dodge, false },
|
{ CounterType.Dodge, false }, // 값을 넣을거에요 -> 회피 카운터 초기값을 거짓(false)으로
|
||||||
{ CounterType.Aim, false },
|
{ CounterType.Aim, false }, // 값을 넣을거에요 -> 조준 카운터 초기값을 거짓(false)으로
|
||||||
{ CounterType.Pierce, false }
|
{ CounterType.Pierce, false } // 값을 넣을거에요 -> 관통 카운터 초기값을 거짓(false)으로
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Decay 타이머 ──
|
// ── Decay 타이머 ──
|
||||||
private Dictionary<CounterType, float> counterTimers = new Dictionary<CounterType, float>()
|
private Dictionary<CounterType, float> counterTimers = new Dictionary<CounterType, float>() // 변수를 선언하고 초기화할거에요 -> 각 카운터의 남은 지속 시간을 저장할 딕셔너리 counterTimers를
|
||||||
{
|
{
|
||||||
{ CounterType.Dodge, 0f },
|
{ CounterType.Dodge, 0f }, // 값을 넣을거에요 -> 회피 카운터 타이머를 0으로
|
||||||
{ CounterType.Aim, 0f },
|
{ CounterType.Aim, 0f }, // 값을 넣을거에요 -> 조준 카운터 타이머를 0으로
|
||||||
{ CounterType.Pierce, 0f }
|
{ CounterType.Pierce, 0f } // 값을 넣을거에요 -> 관통 카운터 타이머를 0으로
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── 쿨타임 ──
|
// ── 쿨타임 ──
|
||||||
private float lastCounterPatternTime = -100f;
|
private float lastCounterPatternTime = -100f; // 변수를 선언할거에요 -> 마지막 카운터 패턴 사용 시간을 lastCounterPatternTime에 (초기값 -100)
|
||||||
|
|
||||||
// ── 습관 변경 보상 추적 ──
|
// ── 습관 변경 보상 추적 ──
|
||||||
private HashSet<CounterType> previousRunCounters = new HashSet<CounterType>();
|
private HashSet<CounterType> previousRunCounters = new HashSet<CounterType>(); // 변수를 선언할거에요 -> 지난 런에서 당했던 카운터 목록을 previousRunCounters에
|
||||||
private HashSet<CounterType> currentRunActivatedCounters = new HashSet<CounterType>();
|
private HashSet<CounterType> currentRunActivatedCounters = new HashSet<CounterType>(); // 변수를 선언할거에요 -> 이번 런에서 발동된 카운터 목록을 currentRunActivatedCounters에
|
||||||
private bool habitChangeRewardGranted = false;
|
private bool habitChangeRewardGranted = false; // 변수를 선언할거에요 -> 보상이 이미 지급되었는지 여부를 habitChangeRewardGranted에
|
||||||
|
|
||||||
// ── 보스 패턴 가중치 정의 ──
|
// ── 보스 패턴 가중치 정의 ──
|
||||||
[System.Serializable]
|
[System.Serializable] // 직렬화할거에요 -> 인스펙터에서 수정할 수 있게 BossPattern 클래스를
|
||||||
public class BossPattern
|
public class BossPattern // 내부 클래스를 선언할거에요 -> 보스 패턴 정보를 담을 BossPattern을
|
||||||
{
|
{
|
||||||
public string patternName;
|
public string patternName; // 변수를 선언할거에요 -> 패턴 이름을 저장할 patternName을
|
||||||
public float baseWeight = 1f;
|
public float baseWeight = 1f; // 변수를 선언할거에요 -> 기본 가중치(확률)를 baseWeight에
|
||||||
[Tooltip("이 패턴이 카운터 패턴인 경우 해당 타입")]
|
[Tooltip("이 패턴이 카운터 패턴인 경우 해당 타입")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
public CounterType counterType = CounterType.None;
|
public CounterType counterType = CounterType.None; // 변수를 선언할거에요 -> 이 패턴이 어떤 카운터 타입인지 지정할 counterType을
|
||||||
[Tooltip("카운터 발동 시 보조적으로 가중치가 올라가는 타입")]
|
[Tooltip("카운터 발동 시 보조적으로 가중치가 올라가는 타입")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
public CounterType subCounterType = CounterType.None;
|
public CounterType subCounterType = CounterType.None; // 변수를 선언할거에요 -> 보너스 가중치를 받을 보조 카운터 타입 subCounterType을
|
||||||
}
|
}
|
||||||
|
|
||||||
[Header("보스 패턴 목록")]
|
[Header("보스 패턴 목록")] // 인스펙터 창에 제목을 표시할거에요 -> 보스 패턴 목록 을
|
||||||
[SerializeField] private List<BossPattern> bossPatterns = new List<BossPattern>();
|
[SerializeField] private List<BossPattern> bossPatterns = new List<BossPattern>(); // 리스트를 선언할거에요 -> 보스의 모든 패턴 정보를 담을 bossPatterns를
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
// 초기화
|
// 초기화
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
private void Start()
|
private void Start() // 함수를 실행할거에요 -> 게임 시작 시 호출되는 Start를
|
||||||
{
|
{
|
||||||
// 🚨 [안전장치] 설정 파일이 없으면 경고 출력
|
// 🚨 [안전장치] 설정 파일이 없으면 경고 출력
|
||||||
if (config == null)
|
if (config == null) // 조건이 맞으면 실행할거에요 -> 설정 파일(config)이 연결되지 않았다면
|
||||||
{
|
{
|
||||||
Debug.LogWarning("⚠ [BossCounterSystem] Config(설정 파일)가 없습니다! AI가 정상 작동하지 않을 수 있습니다.");
|
Debug.LogWarning("⚠ [BossCounterSystem] Config(설정 파일)가 없습니다! AI가 정상 작동하지 않을 수 있습니다."); // 경고 로그를 출력할거에요 -> 설정 파일 누락 경고를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 보스 전투 시작 시 호출. 이전 런에서 발동된 카운터 정보를 기반으로 초기화.
|
/// 보스 전투 시작 시 호출. 이전 런에서 발동된 카운터 정보를 기반으로 초기화.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void InitializeBattle()
|
public void InitializeBattle() // 함수를 선언할거에요 -> 전투 시작 시 초기화를 담당할 InitializeBattle을
|
||||||
{
|
{
|
||||||
// 이전 런에서 잠금 해제된 카운터들을 기록 (습관 변경 보상 판단용)
|
// 이전 런에서 잠금 해제된 카운터들을 기록 (습관 변경 보상 판단용)
|
||||||
previousRunCounters.Clear();
|
previousRunCounters.Clear(); // 비울거에요 -> 이전 런 카운터 목록을
|
||||||
currentRunActivatedCounters.Clear();
|
currentRunActivatedCounters.Clear(); // 비울거에요 -> 이번 런 카운터 목록을
|
||||||
habitChangeRewardGranted = false;
|
habitChangeRewardGranted = false; // 초기화할거에요 -> 보상 지급 여부를 거짓(false)으로
|
||||||
|
|
||||||
var persistence = BossCounterPersistence.Instance;
|
var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소 인스턴스를 persistence에
|
||||||
if (persistence != null)
|
if (persistence != null) // 조건이 맞으면 실행할거에요 -> 저장소가 존재한다면
|
||||||
{
|
{
|
||||||
if (persistence.Data.dodgeCounterActivations > 0 && persistence.IsUnlocked(CounterType.Dodge))
|
if (persistence.Data.dodgeCounterActivations > 0 && persistence.IsUnlocked(CounterType.Dodge)) // 조건이 맞으면 실행할거에요 -> 회피 카운터 기록이 있고 해제되었다면
|
||||||
previousRunCounters.Add(CounterType.Dodge);
|
previousRunCounters.Add(CounterType.Dodge); // 추가할거에요 -> 회피 타입을 이전 런 목록에
|
||||||
if (persistence.Data.aimCounterActivations > 0 && persistence.IsUnlocked(CounterType.Aim))
|
if (persistence.Data.aimCounterActivations > 0 && persistence.IsUnlocked(CounterType.Aim)) // 조건이 맞으면 실행할거에요 -> 조준 카운터 기록이 있고 해제되었다면
|
||||||
previousRunCounters.Add(CounterType.Aim);
|
previousRunCounters.Add(CounterType.Aim); // 추가할거에요 -> 조준 타입을 이전 런 목록에
|
||||||
if (persistence.Data.pierceCounterActivations > 0 && persistence.IsUnlocked(CounterType.Pierce))
|
if (persistence.Data.pierceCounterActivations > 0 && persistence.IsUnlocked(CounterType.Pierce)) // 조건이 맞으면 실행할거에요 -> 관통 카운터 기록이 있고 해제되었다면
|
||||||
previousRunCounters.Add(CounterType.Pierce);
|
previousRunCounters.Add(CounterType.Pierce); // 추가할거에요 -> 관통 타입을 이전 런 목록에
|
||||||
}
|
}
|
||||||
|
|
||||||
// 모든 카운터 모드 OFF
|
// 모든 카운터 모드 OFF
|
||||||
foreach (var type in new[] { CounterType.Dodge, CounterType.Aim, CounterType.Pierce })
|
foreach (var type in new[] { CounterType.Dodge, CounterType.Aim, CounterType.Pierce }) // 반복할거에요 -> 모든 카운터 타입(회피, 조준, 관통)에 대해
|
||||||
{
|
{
|
||||||
activeCounters[type] = false;
|
activeCounters[type] = false; // 상태를 바꿀거에요 -> 해당 카운터를 비활성화(false)로
|
||||||
counterTimers[type] = 0f;
|
counterTimers[type] = 0f; // 값을 바꿀거에요 -> 해당 카운터 타이머를 0으로
|
||||||
}
|
}
|
||||||
|
|
||||||
lastCounterPatternTime = -100f;
|
lastCounterPatternTime = -100f; // 값을 초기화할거에요 -> 마지막 패턴 사용 시간을 -100으로
|
||||||
|
|
||||||
Debug.Log($"[BossCounter] 전투 시작. 이전 런 카운터: [{string.Join(", ", previousRunCounters)}]");
|
Debug.Log($"[BossCounter] 전투 시작. 이전 런 카운터: [{string.Join(", ", previousRunCounters)}]"); // 로그를 출력할거에요 -> 전투 시작 알림과 이전 런 정보를
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
// 매 프레임 업데이트
|
// 매 프레임 업데이트
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
private void Update()
|
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를
|
||||||
{
|
{
|
||||||
// 🚨 [안전장치] 플레이어 트래커나 설정 파일이 없으면 실행 중지 (NullReferenceException 방지)
|
// 🚨 [안전장치] 플레이어 트래커나 설정 파일이 없으면 실행 중지 (NullReferenceException 방지)
|
||||||
if (PlayerBehaviorTracker.Instance == null || config == null) return;
|
if (PlayerBehaviorTracker.Instance == null || config == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어 추적기나 설정 파일이 없다면
|
||||||
|
|
||||||
EvaluateCounters();
|
EvaluateCounters(); // 함수를 실행할거에요 -> 카운터 발동 조건을 검사하는 EvaluateCounters를
|
||||||
DecayCounters();
|
DecayCounters(); // 함수를 실행할거에요 -> 카운터 지속 시간을 관리하는 DecayCounters를
|
||||||
CheckHabitChangeReward();
|
CheckHabitChangeReward(); // 함수를 실행할거에요 -> 습관 변경 보상을 체크하는 CheckHabitChangeReward를
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>플레이어 행동 데이터를 읽고 카운터 모드 ON/OFF 판단</summary>
|
/// <summary>플레이어 행동 데이터를 읽고 카운터 모드 ON/OFF 판단</summary>
|
||||||
private void EvaluateCounters()
|
private void EvaluateCounters() // 함수를 선언할거에요 -> 카운터 발동 여부를 판단하는 EvaluateCounters를
|
||||||
{
|
{
|
||||||
// 🚨 [안전장치] Config가 없으면 계산 불가
|
// 🚨 [안전장치] Config가 없으면 계산 불가
|
||||||
if (config == null) return;
|
if (config == null) return; // 조건이 맞으면 중단할거에요 -> 설정 파일이 없다면
|
||||||
|
|
||||||
var tracker = PlayerBehaviorTracker.Instance;
|
var tracker = PlayerBehaviorTracker.Instance; // 변수를 가져올거에요 -> 플레이어 행동 추적기를 tracker에
|
||||||
var persistence = BossCounterPersistence.Instance;
|
var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소를 persistence에
|
||||||
|
|
||||||
// ── 회피 카운터 ──
|
// ── 회피 카운터 ──
|
||||||
bool dodgeUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Dodge);
|
bool dodgeUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Dodge); // 값을 확인할거에요 -> 회피 카운터가 해금되었는지 여부를
|
||||||
int dodgeThreshold = config.GetEffectiveDodgeThreshold(dodgeUnlocked);
|
int dodgeThreshold = config.GetEffectiveDodgeThreshold(dodgeUnlocked); // 값을 가져올거에요 -> 현재 적용할 회피 임계값을
|
||||||
|
|
||||||
if (tracker.DodgeCount >= dodgeThreshold)
|
if (tracker.DodgeCount >= dodgeThreshold) // 조건이 맞으면 실행할거에요 -> 플레이어 회피 횟수가 임계값 이상이라면
|
||||||
{
|
{
|
||||||
// 잠금 해제 안 된 상태면 첫 발동 시 잠금 해제 (첫 런에서도 발동 가능)
|
// 잠금 해제 안 된 상태면 첫 발동 시 잠금 해제 (첫 런에서도 발동 가능)
|
||||||
// 잠금 해제된 상태면 더 낮은 임계치로 발동
|
// 잠금 해제된 상태면 더 낮은 임계치로 발동
|
||||||
ActivateCounter(CounterType.Dodge);
|
ActivateCounter(CounterType.Dodge); // 함수를 실행할거에요 -> 회피 카운터를 발동시키는 ActivateCounter를
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 조준 카운터 ──
|
// ── 조준 카운터 ──
|
||||||
bool aimUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Aim);
|
bool aimUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Aim); // 값을 확인할거에요 -> 조준 카운터가 해금되었는지 여부를
|
||||||
float aimThreshold = config.GetEffectiveAimThreshold(aimUnlocked);
|
float aimThreshold = config.GetEffectiveAimThreshold(aimUnlocked); // 값을 가져올거에요 -> 현재 적용할 조준 임계값을
|
||||||
|
|
||||||
if (tracker.AimHoldTime >= aimThreshold)
|
if (tracker.AimHoldTime >= aimThreshold) // 조건이 맞으면 실행할거에요 -> 플레이어 조준 시간이 임계값 이상이라면
|
||||||
{
|
{
|
||||||
ActivateCounter(CounterType.Aim);
|
ActivateCounter(CounterType.Aim); // 함수를 실행할거에요 -> 조준 카운터를 발동시키는 ActivateCounter를
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 관통 카운터 ──
|
// ── 관통 카운터 ──
|
||||||
bool pierceUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Pierce);
|
bool pierceUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Pierce); // 값을 확인할거에요 -> 관통 카운터가 해금되었는지 여부를
|
||||||
float pierceThreshold = config.GetEffectivePierceThreshold(pierceUnlocked);
|
float pierceThreshold = config.GetEffectivePierceThreshold(pierceUnlocked); // 값을 가져올거에요 -> 현재 적용할 관통 임계값을
|
||||||
|
|
||||||
if (tracker.TotalShotsInWindow >= config.minShotsForPierceCheck
|
if (tracker.TotalShotsInWindow >= config.minShotsForPierceCheck // 조건이 맞으면 실행할거에요 -> 최소 발사 횟수를 충족하고
|
||||||
&& tracker.PierceRatio >= pierceThreshold)
|
&& tracker.PierceRatio >= pierceThreshold) // 조건이 맞으면 실행할거에요 -> 관통 공격 비율이 임계값 이상이라면
|
||||||
{
|
{
|
||||||
ActivateCounter(CounterType.Pierce);
|
ActivateCounter(CounterType.Pierce); // 함수를 실행할거에요 -> 관통 카운터를 발동시키는 ActivateCounter를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ActivateCounter(CounterType type)
|
private void ActivateCounter(CounterType type) // 함수를 선언할거에요 -> 특정 카운터를 활성화하는 ActivateCounter를
|
||||||
{
|
{
|
||||||
// 🚨 [안전장치] Config 확인
|
// 🚨 [안전장치] Config 확인
|
||||||
if (config == null) return;
|
if (config == null) return; // 조건이 맞으면 중단할거에요 -> 설정 파일이 없다면
|
||||||
|
|
||||||
counterTimers[type] = config.counterDecayTime;
|
counterTimers[type] = config.counterDecayTime; // 값을 설정할거에요 -> 해당 카운터의 지속 시간을 설정값으로
|
||||||
|
|
||||||
if (!activeCounters[type])
|
if (!activeCounters[type]) // 조건이 맞으면 실행할거에요 -> 해당 카운터가 현재 꺼져 있다면
|
||||||
{
|
{
|
||||||
activeCounters[type] = true;
|
activeCounters[type] = true; // 상태를 바꿀거에요 -> 카운터 활성 상태를 참(true)으로
|
||||||
currentRunActivatedCounters.Add(type);
|
currentRunActivatedCounters.Add(type); // 추가할거에요 -> 이번 런 발동 목록에 해당 타입을
|
||||||
|
|
||||||
// 영구 잠금 해제
|
// 영구 잠금 해제
|
||||||
var persistence = BossCounterPersistence.Instance;
|
var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소를 persistence에
|
||||||
if (persistence != null)
|
if (persistence != null) // 조건이 맞으면 실행할거에요 -> 저장소가 있다면
|
||||||
{
|
{
|
||||||
persistence.UnlockCounter(type);
|
persistence.UnlockCounter(type); // 함수를 실행할거에요 -> 해당 카운터를 영구 해금하는 UnlockCounter를
|
||||||
persistence.RecordActivation(type);
|
persistence.RecordActivation(type); // 함수를 실행할거에요 -> 발동 횟수를 기록하는 RecordActivation을
|
||||||
}
|
}
|
||||||
|
|
||||||
OnCounterActivated?.Invoke(type);
|
OnCounterActivated?.Invoke(type); // 이벤트를 실행할거에요 -> 카운터 활성화 알림 이벤트를
|
||||||
Debug.Log($"[BossCounter] {type} 카운터 활성화!");
|
Debug.Log($"[BossCounter] {type} 카운터 활성화!"); // 로그를 출력할거에요 -> 카운터 활성화 메시지를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>시간이 지나면 카운터 모드 자동 해제</summary>
|
/// <summary>시간이 지나면 카운터 모드 자동 해제</summary>
|
||||||
private void DecayCounters()
|
private void DecayCounters() // 함수를 선언할거에요 -> 시간이 지나면 카운터를 끄는 DecayCounters를
|
||||||
{
|
{
|
||||||
foreach (var type in new[] { CounterType.Dodge, CounterType.Aim, CounterType.Pierce })
|
foreach (var type in new[] { CounterType.Dodge, CounterType.Aim, CounterType.Pierce }) // 반복할거에요 -> 모든 카운터 타입에 대해
|
||||||
{
|
{
|
||||||
if (!activeCounters[type]) continue;
|
if (!activeCounters[type]) continue; // 조건이 맞으면 건너뛸거에요 -> 이미 꺼져 있는 카운터라면
|
||||||
|
|
||||||
counterTimers[type] -= Time.deltaTime;
|
counterTimers[type] -= Time.deltaTime; // 값을 뺄거에요 -> 남은 시간에서 프레임 시간을
|
||||||
if (counterTimers[type] <= 0f)
|
if (counterTimers[type] <= 0f) // 조건이 맞으면 실행할거에요 -> 남은 시간이 0 이하라면
|
||||||
{
|
{
|
||||||
activeCounters[type] = false;
|
activeCounters[type] = false; // 상태를 바꿀거에요 -> 카운터 활성 상태를 거짓(false)으로
|
||||||
OnCounterDeactivated?.Invoke(type);
|
OnCounterDeactivated?.Invoke(type); // 이벤트를 실행할거에요 -> 카운터 비활성화 알림 이벤트를
|
||||||
Debug.Log($"[BossCounter] {type} 카운터 Decay로 비활성화");
|
Debug.Log($"[BossCounter] {type} 카운터 Decay로 비활성화"); // 로그를 출력할거에요 -> 시간 만료로 인한 비활성화 메시지를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -220,25 +213,25 @@ public class BossCounterSystem : MonoBehaviour
|
||||||
/// 이전 런에서 카운터 당한 습관을 이번 런에서 보이지 않으면 보상.
|
/// 이전 런에서 카운터 당한 습관을 이번 런에서 보이지 않으면 보상.
|
||||||
/// 전투 중반(30초 이후) 한 번 체크.
|
/// 전투 중반(30초 이후) 한 번 체크.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private float battleTimer = 0f;
|
private float battleTimer = 0f; // 변수를 초기화할거에요 -> 전투 진행 시간을 0으로
|
||||||
private const float HABIT_CHECK_TIME = 30f;
|
private const float HABIT_CHECK_TIME = 30f; // 상수를 정의할거에요 -> 습관 체크 시점을 30초로
|
||||||
|
|
||||||
private void CheckHabitChangeReward()
|
private void CheckHabitChangeReward() // 함수를 선언할거에요 -> 습관 변경 보상을 확인하는 CheckHabitChangeReward를
|
||||||
{
|
{
|
||||||
if (habitChangeRewardGranted || previousRunCounters.Count == 0) return;
|
if (habitChangeRewardGranted || previousRunCounters.Count == 0) return; // 조건이 맞으면 중단할거에요 -> 이미 보상을 받았거나 이전 런 기록이 없다면
|
||||||
|
|
||||||
battleTimer += Time.deltaTime;
|
battleTimer += Time.deltaTime; // 값을 더할거에요 -> 전투 진행 시간에 프레임 시간을
|
||||||
if (battleTimer < HABIT_CHECK_TIME) return;
|
if (battleTimer < HABIT_CHECK_TIME) return; // 조건이 맞으면 중단할거에요 -> 아직 30초가 안 지났다면
|
||||||
|
|
||||||
// 이전 런에서 발동된 카운터 중, 이번 런에서 아직 발동 안 된 것이 있으면 보상
|
// 이전 런에서 발동된 카운터 중, 이번 런에서 아직 발동 안 된 것이 있으면 보상
|
||||||
foreach (var prevCounter in previousRunCounters)
|
foreach (var prevCounter in previousRunCounters) // 반복할거에요 -> 이전 런 카운터 목록에 대해
|
||||||
{
|
{
|
||||||
if (!currentRunActivatedCounters.Contains(prevCounter))
|
if (!currentRunActivatedCounters.Contains(prevCounter)) // 조건이 맞으면 실행할거에요 -> 이번 런 목록에 해당 카운터가 없다면 (습관 고침)
|
||||||
{
|
{
|
||||||
habitChangeRewardGranted = true;
|
habitChangeRewardGranted = true; // 상태를 바꿀거에요 -> 보상 지급 여부를 참(true)으로
|
||||||
OnHabitChangeRewarded?.Invoke();
|
OnHabitChangeRewarded?.Invoke(); // 이벤트를 실행할거에요 -> 보상 지급 알림 이벤트를
|
||||||
Debug.Log($"[BossCounter] 플레이어가 {prevCounter} 습관을 바꿈! 보상 부여");
|
Debug.Log($"[BossCounter] 플레이어가 {prevCounter} 습관을 바꿈! 보상 부여"); // 로그를 출력할거에요 -> 보상 부여 메시지를
|
||||||
break;
|
break; // 중단할거에요 -> 보상은 한 번만 주므로 반복문을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -252,106 +245,106 @@ public class BossCounterSystem : MonoBehaviour
|
||||||
/// 보스 AI의 패턴 선택 시점에 호출하세요.
|
/// 보스 AI의 패턴 선택 시점에 호출하세요.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>선택된 패턴 이름</returns>
|
/// <returns>선택된 패턴 이름</returns>
|
||||||
public string SelectBossPattern()
|
public string SelectBossPattern() // 함수를 선언할거에요 -> 보스 패턴을 선택해서 반환하는 SelectBossPattern을
|
||||||
{
|
{
|
||||||
if (bossPatterns.Count == 0)
|
if (bossPatterns.Count == 0) // 조건이 맞으면 실행할거에요 -> 등록된 패턴이 하나도 없다면
|
||||||
{
|
{
|
||||||
Debug.LogWarning("[BossCounter] 보스 패턴이 등록되지 않았습니다!");
|
Debug.LogWarning("[BossCounter] 보스 패턴이 등록되지 않았습니다!"); // 경고 로그를 출력할거에요 -> 패턴 없음 경고를
|
||||||
return "Normal"; // 기본값 리턴
|
return "Normal"; // 값을 반환할거에요 -> 기본값 "Normal"을
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🚨 [안전장치] Config가 없으면 그냥 랜덤 패턴 반환 (멈춤 방지)
|
// 🚨 [안전장치] Config가 없으면 그냥 랜덤 패턴 반환 (멈춤 방지)
|
||||||
if (config == null)
|
if (config == null) // 조건이 맞으면 실행할거에요 -> 설정 파일이 없다면
|
||||||
{
|
{
|
||||||
int randomIndex = Random.Range(0, bossPatterns.Count);
|
int randomIndex = Random.Range(0, bossPatterns.Count); // 값을 랜덤으로 뽑을거에요 -> 0부터 패턴 개수 사이의 인덱스를
|
||||||
return bossPatterns[randomIndex].patternName;
|
return bossPatterns[randomIndex].patternName; // 값을 반환할거에요 -> 랜덤하게 뽑힌 패턴 이름을
|
||||||
}
|
}
|
||||||
|
|
||||||
// 가중치 계산
|
// 가중치 계산
|
||||||
float[] weights = new float[bossPatterns.Count];
|
float[] weights = new float[bossPatterns.Count]; // 배열을 만들거에요 -> 각 패턴의 가중치를 담을 weights 배열을
|
||||||
float totalWeight = 0f;
|
float totalWeight = 0f; // 변수를 초기화할거에요 -> 전체 가중치 합계를 0으로
|
||||||
float counterWeight = 0f;
|
float counterWeight = 0f; // 변수를 초기화할거에요 -> 카운터 패턴들의 가중치 합계를 0으로
|
||||||
|
|
||||||
for (int i = 0; i < bossPatterns.Count; i++)
|
for (int i = 0; i < bossPatterns.Count; i++) // 반복할거에요 -> 모든 패턴에 대해
|
||||||
{
|
{
|
||||||
weights[i] = bossPatterns[i].baseWeight;
|
weights[i] = bossPatterns[i].baseWeight; // 값을 넣을거에요 -> 기본 가중치를 배열에
|
||||||
|
|
||||||
// 카운터 패턴 가중치 증가
|
// 카운터 패턴 가중치 증가
|
||||||
if (bossPatterns[i].counterType != CounterType.None
|
if (bossPatterns[i].counterType != CounterType.None // 조건이 맞으면 실행할거에요 -> 카운터 타입이 설정되어 있고
|
||||||
&& activeCounters.ContainsKey(bossPatterns[i].counterType)
|
&& activeCounters.ContainsKey(bossPatterns[i].counterType) // 조건이 맞으면 실행할거에요 -> 딕셔너리에 키가 존재하며
|
||||||
&& activeCounters[bossPatterns[i].counterType])
|
&& activeCounters[bossPatterns[i].counterType]) // 조건이 맞으면 실행할거에요 -> 해당 카운터가 활성 상태라면
|
||||||
{
|
{
|
||||||
// 쿨타임 체크
|
// 쿨타임 체크
|
||||||
if (Time.time - lastCounterPatternTime >= config.counterCooldown)
|
if (Time.time - lastCounterPatternTime >= config.counterCooldown) // 조건이 맞으면 실행할거에요 -> 마지막 사용 후 쿨타임이 지났다면
|
||||||
{
|
{
|
||||||
weights[i] += config.counterWeightBonus;
|
weights[i] += config.counterWeightBonus; // 값을 더할거에요 -> 카운터 보너스 가중치를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 보조 카운터 가중치
|
// 보조 카운터 가중치
|
||||||
if (bossPatterns[i].subCounterType != CounterType.None
|
if (bossPatterns[i].subCounterType != CounterType.None // 조건이 맞으면 실행할거에요 -> 보조 카운터 타입이 설정되어 있고
|
||||||
&& activeCounters.ContainsKey(bossPatterns[i].subCounterType)
|
&& activeCounters.ContainsKey(bossPatterns[i].subCounterType) // 조건이 맞으면 실행할거에요 -> 딕셔너리에 키가 존재하며
|
||||||
&& activeCounters[bossPatterns[i].subCounterType])
|
&& activeCounters[bossPatterns[i].subCounterType]) // 조건이 맞으면 실행할거에요 -> 해당 보조 카운터가 활성 상태라면
|
||||||
{
|
{
|
||||||
weights[i] += config.counterSubWeightBonus;
|
weights[i] += config.counterSubWeightBonus; // 값을 더할거에요 -> 보조 카운터 보너스 가중치를
|
||||||
}
|
}
|
||||||
|
|
||||||
// 습관 변경 보상: 일반 패턴 가중치 감소 (= 난이도 완화)
|
// 습관 변경 보상: 일반 패턴 가중치 감소 (= 난이도 완화)
|
||||||
if (habitChangeRewardGranted && bossPatterns[i].counterType == CounterType.None)
|
if (habitChangeRewardGranted && bossPatterns[i].counterType == CounterType.None) // 조건이 맞으면 실행할거에요 -> 보상을 받았고 일반 패턴이라면
|
||||||
{
|
{
|
||||||
weights[i] *= (1f - config.habitChangeRewardRatio);
|
weights[i] *= (1f - config.habitChangeRewardRatio); // 값을 곱할거에요 -> 가중치를 감소 비율만큼 줄여서
|
||||||
}
|
}
|
||||||
|
|
||||||
totalWeight += weights[i];
|
totalWeight += weights[i]; // 값을 더할거에요 -> 전체 가중치 합계에 현재 가중치를
|
||||||
|
|
||||||
if (bossPatterns[i].counterType != CounterType.None)
|
if (bossPatterns[i].counterType != CounterType.None) // 조건이 맞으면 실행할거에요 -> 카운터 패턴이라면
|
||||||
counterWeight += weights[i];
|
counterWeight += weights[i]; // 값을 더할거에요 -> 카운터 가중치 합계에
|
||||||
}
|
}
|
||||||
|
|
||||||
// 빈도 상한 체크: 카운터 패턴 총 비율이 maxCounterFrequency를 넘지 않도록
|
// 빈도 상한 체크: 카운터 패턴 총 비율이 maxCounterFrequency를 넘지 않도록
|
||||||
if (totalWeight > 0f && counterWeight / totalWeight > config.maxCounterFrequency)
|
if (totalWeight > 0f && counterWeight / totalWeight > config.maxCounterFrequency) // 조건이 맞으면 실행할거에요 -> 카운터 패턴 비율이 최대 허용치를 초과한다면
|
||||||
{
|
{
|
||||||
float allowedCounterWeight = (totalWeight - counterWeight) * config.maxCounterFrequency / (1f - config.maxCounterFrequency);
|
float allowedCounterWeight = (totalWeight - counterWeight) * config.maxCounterFrequency / (1f - config.maxCounterFrequency); // 값을 계산할거에요 -> 허용 가능한 카운터 총 가중치를
|
||||||
float scale = allowedCounterWeight / counterWeight;
|
float scale = allowedCounterWeight / counterWeight; // 값을 계산할거에요 -> 가중치를 줄일 비율(scale)을
|
||||||
|
|
||||||
for (int i = 0; i < bossPatterns.Count; i++)
|
for (int i = 0; i < bossPatterns.Count; i++) // 반복할거에요 -> 모든 패턴에 대해
|
||||||
{
|
{
|
||||||
if (bossPatterns[i].counterType != CounterType.None)
|
if (bossPatterns[i].counterType != CounterType.None) // 조건이 맞으면 실행할거에요 -> 카운터 패턴이라면
|
||||||
{
|
{
|
||||||
float bonus = weights[i] - bossPatterns[i].baseWeight;
|
float bonus = weights[i] - bossPatterns[i].baseWeight; // 값을 계산할거에요 -> 추가된 보너스 가중치를
|
||||||
weights[i] = bossPatterns[i].baseWeight + bonus * scale;
|
weights[i] = bossPatterns[i].baseWeight + bonus * scale; // 값을 수정할거에요 -> 보너스를 비율대로 줄여서 다시 적용
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 총 가중치 재계산
|
// 총 가중치 재계산
|
||||||
totalWeight = 0f;
|
totalWeight = 0f; // 값을 초기화할거에요 -> 전체 합계를 0으로
|
||||||
for (int i = 0; i < weights.Length; i++)
|
for (int i = 0; i < weights.Length; i++) // 반복할거에요 -> 모든 가중치에 대해
|
||||||
totalWeight += weights[i];
|
totalWeight += weights[i]; // 값을 더할거에요 -> 전체 합계에
|
||||||
}
|
}
|
||||||
|
|
||||||
// 가중치 랜덤 선택
|
// 가중치 랜덤 선택
|
||||||
float roll = Random.Range(0f, totalWeight);
|
float roll = Random.Range(0f, totalWeight); // 값을 랜덤으로 뽑을거에요 -> 0부터 전체 가중치 사이의 값을 roll에
|
||||||
float cumulative = 0f;
|
float cumulative = 0f; // 변수를 초기화할거에요 -> 누적 가중치를 0으로
|
||||||
|
|
||||||
for (int i = 0; i < bossPatterns.Count; i++)
|
for (int i = 0; i < bossPatterns.Count; i++) // 반복할거에요 -> 모든 패턴에 대해
|
||||||
{
|
{
|
||||||
cumulative += weights[i];
|
cumulative += weights[i]; // 값을 더할거에요 -> 누적 가중치에 현재 가중치를
|
||||||
if (roll <= cumulative)
|
if (roll <= cumulative) // 조건이 맞으면 실행할거에요 -> 랜덤값이 누적치보다 작거나 같다면 (당첨)
|
||||||
{
|
{
|
||||||
string selected = bossPatterns[i].patternName;
|
string selected = bossPatterns[i].patternName; // 값을 저장할거에요 -> 선택된 패턴 이름을 selected에
|
||||||
|
|
||||||
// 카운터 패턴이면 쿨타임 기록
|
// 카운터 패턴이면 쿨타임 기록
|
||||||
if (bossPatterns[i].counterType != CounterType.None)
|
if (bossPatterns[i].counterType != CounterType.None) // 조건이 맞으면 실행할거에요 -> 카운터 패턴이라면
|
||||||
{
|
{
|
||||||
lastCounterPatternTime = Time.time;
|
lastCounterPatternTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 사용 시간으로
|
||||||
}
|
}
|
||||||
|
|
||||||
OnCounterPatternSelected?.Invoke(selected);
|
OnCounterPatternSelected?.Invoke(selected); // 이벤트를 실행할거에요 -> 패턴 선택 알림 이벤트를
|
||||||
return selected;
|
return selected; // 값을 반환할거에요 -> 선택된 패턴 이름을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return bossPatterns[bossPatterns.Count - 1].patternName;
|
return bossPatterns[bossPatterns.Count - 1].patternName; // 값을 반환할거에요 -> 만약 선택되지 않았다면 마지막 패턴을 (안전장치)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
@ -359,22 +352,22 @@ public class BossCounterSystem : MonoBehaviour
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
/// <summary>특정 카운터 모드가 현재 활성 상태인지 확인</summary>
|
/// <summary>특정 카운터 모드가 현재 활성 상태인지 확인</summary>
|
||||||
public bool IsCounterActive(CounterType type)
|
public bool IsCounterActive(CounterType type) // 함수를 선언할거에요 -> 특정 카운터 활성 여부를 반환하는 IsCounterActive를
|
||||||
{
|
{
|
||||||
return activeCounters.ContainsKey(type) && activeCounters[type];
|
return activeCounters.ContainsKey(type) && activeCounters[type]; // 값을 반환할거에요 -> 딕셔너리에 키가 있고 값이 참(true)인지를
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>현재 활성화된 모든 카운터 타입 반환</summary>
|
/// <summary>현재 활성화된 모든 카운터 타입 반환</summary>
|
||||||
public List<CounterType> GetActiveCounters()
|
public List<CounterType> GetActiveCounters() // 함수를 선언할거에요 -> 활성화된 모든 카운터 목록을 반환하는 GetActiveCounters를
|
||||||
{
|
{
|
||||||
var result = new List<CounterType>();
|
var result = new List<CounterType>(); // 리스트를 만들거에요 -> 결과를 담을 result 리스트를
|
||||||
foreach (var kvp in activeCounters)
|
foreach (var kvp in activeCounters) // 반복할거에요 -> 모든 카운터 상태에 대해
|
||||||
{
|
{
|
||||||
if (kvp.Value) result.Add(kvp.Key);
|
if (kvp.Value) result.Add(kvp.Key); // 조건이 맞으면 실행할거에요 -> 활성화(true) 상태라면 리스트에 추가하기를
|
||||||
}
|
}
|
||||||
return result;
|
return result; // 값을 반환할거에요 -> 완성된 리스트를
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>습관 변경 보상이 활성화되었는지 확인</summary>
|
/// <summary>습관 변경 보상이 활성화되었는지 확인</summary>
|
||||||
public bool IsHabitChangeRewarded => habitChangeRewardGranted;
|
public bool IsHabitChangeRewarded => habitChangeRewardGranted; // 프로퍼티를 선언할거에요 -> 보상 지급 여부를 외부에서 읽을 수 있는 IsHabitChangeRewarded를
|
||||||
}
|
}
|
||||||
|
|
@ -1,460 +1,443 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using System.Collections;
|
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
||||||
|
|
||||||
public class NorcielBoss : MonsterClass
|
public class NorcielBoss : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 NorcielBoss를
|
||||||
{
|
{
|
||||||
[Header("--- 🧠 두뇌 연결 ---")]
|
[Header("--- 🧠 두뇌 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🧠 두뇌 연결 ---을
|
||||||
[SerializeField] private BossCounterSystem counterSystem;
|
[SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 보스의 패턴을 결정할 counterSystem을
|
||||||
|
|
||||||
[Header("--- ⚔️ 패턴 설정 ---")]
|
[Header("--- ⚔️ 패턴 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- ⚔️ 패턴 설정 ---을
|
||||||
[SerializeField] private float patternInterval = 3f;
|
[SerializeField] private float patternInterval = 3f; // 변수를 선언할거에요 -> 공격 후 다음 공격까지 대기 시간을 3초로 지정하는 patternInterval을
|
||||||
|
[SerializeField] private float attackRange = 3f; // 변수를 선언할거에요 -> 이 거리 안에 플레이어가 들어오면 공격하는 attackRange를 3으로
|
||||||
|
|
||||||
[Header("--- 🎱 무기(쇠공) 설정 (필수!) ---")]
|
[Header("--- 🎱 무기(쇠공) 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🎱 무기 설정 ---을
|
||||||
[SerializeField] private GameObject ironBall;
|
[SerializeField] private GameObject ironBall; // 변수를 선언할거에요 -> 던지고 주울 무기 오브젝트인 ironBall을
|
||||||
[SerializeField] private Transform handHolder;
|
[SerializeField] private Transform handHolder; // 변수를 선언할거에요 -> 무기를 잡을 손의 위치인 handHolder를
|
||||||
|
|
||||||
[Header("--- 📊 UI 연결 ---")]
|
[Header("--- 📊 UI 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 📊 UI 연결 ---을
|
||||||
[SerializeField] private GameObject bossHealthBar;
|
[SerializeField] private GameObject bossHealthBar; // 변수를 선언할거에요 -> 보스의 체력 UI를 담을 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_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;
|
private float _timer; // 변수를 선언할거에요 -> 공격 쿨타임을 계산할 내부 타이머 _timer를
|
||||||
private Rigidbody rb;
|
private Rigidbody rb; // 변수를 선언할거에요 -> 보스의 물리 엔진을 제어할 rb를
|
||||||
private Rigidbody ballRb;
|
private Rigidbody ballRb; // 변수를 선언할거에요 -> 쇠공의 물리 엔진을 제어할 ballRb를
|
||||||
|
|
||||||
private bool isBattleStarted = false;
|
private bool isBattleStarted = false; // 변수를 초기화할거에요 -> 전투 시작 상태를 거짓(false)으로
|
||||||
private bool isWeaponless = false;
|
private bool isWeaponless = false; // 변수를 초기화할거에요 -> 맨손 상태를 거짓(false)으로
|
||||||
private bool isPerformingAction = false; // ⭐ 신규: 연출 중 플래그 (Roar 등)
|
private bool isPerformingAction = false; // 변수를 초기화할거에요 -> 연출 진행 상태를 거짓(false)으로
|
||||||
private Transform target;
|
private Transform target; // 변수를 선언할거에요 -> 보스가 쫓아갈 대상의 위치인 target을
|
||||||
|
|
||||||
protected override void Awake()
|
protected override void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 가장 먼저 호출되는 Awake를
|
||||||
{
|
{
|
||||||
base.Awake();
|
base.Awake(); // 부모 클래스의 함수를 먼저 실행할거에요 -> MonsterClass의 Awake 로직을
|
||||||
rb = GetComponent<Rigidbody>();
|
rb = GetComponent<Rigidbody>(); // 컴포넌트를 가져와서 저장할거에요 -> 보스 자신의 Rigidbody를 rb에
|
||||||
if (ironBall != null) ballRb = ironBall.GetComponent<Rigidbody>();
|
if (ironBall != null) ballRb = ironBall.GetComponent<Rigidbody>(); // 조건이 맞으면 가져올거에요 -> ironBall이 있다면 그것의 Rigidbody를 ballRb에
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⭐ [수정] Start → OnEnable 이후 호출되도록 변경
|
private void Start() // 함수를 실행할거에요 -> 첫 프레임이 시작될 때 호출되는 Start를
|
||||||
private void Start()
|
|
||||||
{
|
{
|
||||||
StartBossBattle();
|
StartBossBattle(); // 함수를 실행할거에요 -> Roar 연출 후 전투를 시작하는 StartBossBattle을
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Init()
|
protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 몬스터 초기화를 담당하는 Init을
|
||||||
{
|
{
|
||||||
_timer = patternInterval;
|
base.Init(); // 부모 초기화 호출
|
||||||
isBattleStarted = false;
|
_timer = 0f; // 값을 넣을거에요 -> 전투 시작하자마자 첫 공격이 가능하게 타이머를 0으로
|
||||||
isWeaponless = false;
|
isBattleStarted = false; // 상태를 바꿀거에요 -> 전투 시작 상태를 거짓(false)으로
|
||||||
isPerformingAction = false;
|
isWeaponless = false; // 상태를 바꿀거에요 -> 맨손 상태를 거짓(false)으로
|
||||||
|
isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 상태를 거짓(false)으로
|
||||||
|
|
||||||
// 플레이어 찾기
|
IsAggroed = true; // 상태를 바꿀거에요 -> 부모 클래스의 영구 어그로 상태를 참(true)으로
|
||||||
GameObject playerObj = GameObject.FindWithTag("Player");
|
mobRenderer = null; // 값을 지울거에요 -> 화면 밖 최적화를 끄기 위해 렌더러를 null로
|
||||||
if (playerObj != null) target = playerObj.transform;
|
optimizationDistance = 9999f; // 값을 바꿀거에요 -> 거리가 멀어져도 멈추지 않게 최적화 거리를 9999로
|
||||||
else
|
|
||||||
|
GameObject playerObj = GameObject.FindWithTag("Player"); // 오브젝트를 찾을거에요 -> Player 태그가 붙은 녀석을
|
||||||
|
if (playerObj != null) // 조건이 맞으면 실행할거에요 -> 태그로 찾은 플레이어가 존재한다면
|
||||||
{
|
{
|
||||||
var playerScript = FindObjectOfType<PlayerMovement>();
|
target = playerObj.transform; // 값을 넣을거에요 -> 플레이어의 위치를 target에
|
||||||
if (playerScript != null) target = playerScript.transform;
|
playerTransform = playerObj.transform; // 값을 넣을거에요 -> 부모의 추적 변수에도 플레이어의 위치를
|
||||||
|
}
|
||||||
|
else // 조건이 틀리면 실행할거에요 -> 태그로 찾지 못했다면
|
||||||
|
{
|
||||||
|
var playerScript = FindObjectOfType<PlayerMovement>(); // 컴포넌트를 찾을거에요 -> PlayerMovement 스크립트를 가진 오브젝트를
|
||||||
|
if (playerScript != null) // 조건이 맞으면 실행할거에요 -> 스크립트로 플레이어를 찾았다면
|
||||||
|
{
|
||||||
|
target = playerScript.transform; // 값을 넣을거에요 -> 플레이어의 위치를 target에
|
||||||
|
playerTransform = playerScript.transform; // 값을 넣을거에요 -> 부모의 추적 변수에도 플레이어의 위치를
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HP 초기화
|
if (target == null) // 조건이 맞으면 실행할거에요 -> 어떤 방법으로도 플레이어를 못 찾았다면
|
||||||
currentHP = maxHP;
|
Debug.LogError("❌ [Boss] 플레이어를 찾지 못했습니다! Player 태그 또는 PlayerMovement 확인!"); // 에러를 출력할거에요 -> 플레이어 없음 경고 메시지를
|
||||||
|
|
||||||
StartCoroutine(SafeInitNavMesh());
|
currentHP = maxHP; // 체력을 채울거에요 -> 현재 체력을 최대 체력으로
|
||||||
|
StartCoroutine(SafeInitNavMesh()); // 코루틴을 실행할거에요 -> 길찾기를 안전하게 켜는 SafeInitNavMesh를
|
||||||
|
|
||||||
if (bossHealthBar != null) bossHealthBar.SetActive(false);
|
if (bossHealthBar != null) bossHealthBar.SetActive(false); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에서 끄기를
|
||||||
if (ballRb != null) ballRb.isKinematic = true;
|
if (ballRb != null) ballRb.isKinematic = true; // 조건이 맞으면 실행할거에요 -> 쇠공 물리가 있다면 중력 연산을 끄기를
|
||||||
|
if (animator != null) animator.Play(anim_Idle); // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 대기 모션을
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerator SafeInitNavMesh()
|
private IEnumerator SafeInitNavMesh() // 코루틴 함수를 선언할거에요 -> 길찾기를 안전하게 초기화하는 SafeInitNavMesh를
|
||||||
{
|
{
|
||||||
yield return null;
|
yield return null; // 기다릴거에요 -> 다음 프레임이 될 때까지 한 턴을
|
||||||
|
if (agent != null) // 조건이 맞으면 실행할거에요 -> NavMeshAgent가 존재한다면
|
||||||
if (agent != null)
|
|
||||||
{
|
{
|
||||||
int retry = 0;
|
int retry = 0; // 변수를 선언할거에요 -> 재시도 횟수를 셀 retry를 0으로
|
||||||
while (!agent.isOnNavMesh && retry < 5)
|
while (!agent.isOnNavMesh && retry < 5) // 반복할거에요 -> 바닥에 없고 시도가 5번 미만인 동안
|
||||||
{
|
{
|
||||||
retry++;
|
retry++; // 값을 증가시킬거에요 -> 재시도 횟수를 1만큼
|
||||||
yield return new WaitForSeconds(0.1f);
|
yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초의 시간을
|
||||||
|
}
|
||||||
|
if (agent.isOnNavMesh) agent.isStopped = true; // 조건이 맞으면 실행할거에요 -> 바닥에 닿았다면 이동을 정지로
|
||||||
|
agent.enabled = false; // 기능을 끌거에요 -> 전투 시작 전까지 NavMeshAgent를 비활성화로
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (agent.isOnNavMesh)
|
protected override void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를
|
||||||
{
|
{
|
||||||
agent.isStopped = true;
|
base.Update(); // 부모 클래스의 Update도 호출 (최적화 로직 등)
|
||||||
}
|
|
||||||
agent.enabled = false;
|
if (!isDead) // 조건이 맞으면 실행할거에요 -> 보스가 살아있다면
|
||||||
|
{
|
||||||
|
ExecuteAILogic(); // 함수를 실행할거에요 -> 보스 AI 두뇌인 ExecuteAILogic을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════
|
// ════════════════════════════════════════
|
||||||
// ⭐ [핵심 수정] 부모의 OnManagedUpdate 흐름 제어
|
// 🧠 AI 핵심 로직
|
||||||
// ════════════════════════════════════════
|
// ════════════════════════════════════════
|
||||||
|
|
||||||
/// <summary>
|
protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> 보스 AI 두뇌인 ExecuteAILogic을
|
||||||
/// 부모 MonsterClass의 StopMovement()가 매 프레임 Monster_Idle을
|
|
||||||
/// 강제 재생해서 Roar/다른 애니메이션을 덮어쓰는 문제 해결.
|
|
||||||
///
|
|
||||||
/// 보스는 일반 몬스터와 다르게:
|
|
||||||
/// - 전투 시작 전에는 아무 AI도 돌리지 않음
|
|
||||||
/// - 연출 중(Roar 등)에는 애니메이션을 건드리지 않음
|
|
||||||
/// </summary>
|
|
||||||
protected override void ExecuteAILogic()
|
|
||||||
{
|
{
|
||||||
// 연출 중이거나 전투 미시작 → 아무것도 안 함
|
if (isPerformingAction || !isBattleStarted || target == null) return; // 조건이 맞으면 중단할거에요 -> 연출중, 전투전, 타겟없음 중 하나라도 해당되면
|
||||||
if (isPerformingAction || !isBattleStarted || target == null) return;
|
|
||||||
|
|
||||||
// 무기 없으면 회수 우선
|
if (isWeaponless) // 조건이 맞으면 실행할거에요 -> 쇠공을 던져서 맨손이라면
|
||||||
if (isWeaponless)
|
|
||||||
{
|
{
|
||||||
RetrieveWeaponLogic();
|
RetrieveWeaponLogic(); // 함수를 실행할거에요 -> 쇠공을 주우러 가는 로직을
|
||||||
return;
|
return; // 중단할거에요 -> 무기 줍기가 우선이므로 아래 로직을
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 일반 전투 로직 ---
|
if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면 AI 판단을
|
||||||
_timer -= Time.deltaTime;
|
|
||||||
if (_timer <= 0 && !isAttacking && !isHit && !isDead && !isResting)
|
if (_timer > 0) // 조건이 맞으면 실행할거에요 -> 공격 쿨타임이 남아있다면
|
||||||
{
|
{
|
||||||
_timer = patternInterval;
|
_timer -= Time.deltaTime; // 값을 뺄거에요 -> 쿨타임 타이머에서 지난 프레임 시간만큼을
|
||||||
DecideAttack();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 이동 (공격/피격 중이 아닐 때만)
|
float distToPlayer = Vector3.Distance(transform.position, target.position); // 거리를 계산할거에요 -> 보스와 플레이어 사이의 거리를
|
||||||
if (!isAttacking && !isHit && !isResting && agent != null && agent.enabled && agent.isOnNavMesh)
|
|
||||||
{
|
|
||||||
agent.SetDestination(target.position);
|
|
||||||
|
|
||||||
if (animator != null) animator.SetFloat("Speed", agent.velocity.magnitude);
|
if (agent == null || !agent.enabled || !agent.isOnNavMesh) return; // 조건이 맞으면 중단할거에요 -> 길찾기가 불가능한 상태라면
|
||||||
|
|
||||||
if (agent.remainingDistance <= agent.stoppingDistance + 0.5f)
|
if (distToPlayer > attackRange) // 조건이 맞으면 실행할거에요 -> 플레이어가 공격 사거리 밖이라면
|
||||||
{
|
{
|
||||||
agent.isStopped = true;
|
agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 걸어가라고
|
||||||
LookAtTarget(target.position);
|
agent.SetDestination(target.position); // 명령을 내릴거에요 -> 길찾기 목표를 플레이어 위치로
|
||||||
if (animator != null) animator.SetFloat("Speed", 0f);
|
LookAtTarget(target.position); // 함수를 실행할거에요 -> 플레이어를 바라보는 LookAtTarget을
|
||||||
|
UpdateMovementAnimation(); // 함수를 실행할거에요 -> 걷기 애니메이션을 맞추는 UpdateMovementAnimation을
|
||||||
}
|
}
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 플레이어가 사거리 안에 있다면
|
||||||
{
|
{
|
||||||
agent.isStopped = false;
|
agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고
|
||||||
}
|
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 이동 속도를 완전한 0으로
|
||||||
}
|
LookAtTarget(target.position); // 함수를 실행할거에요 -> 플레이어를 바라보는 LookAtTarget을
|
||||||
}
|
|
||||||
|
|
||||||
// ════════════════════════════════════════
|
if (animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 존재한다면
|
||||||
// 🏃♂️ 무기 회수 로직
|
|
||||||
// ════════════════════════════════════════
|
|
||||||
|
|
||||||
private void RetrieveWeaponLogic()
|
|
||||||
{
|
{
|
||||||
if (ironBall == null || !agent.enabled || !agent.isOnNavMesh) return;
|
AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 변수를 선언할거에요 -> 현재 재생 중인 애니메이션 상태를
|
||||||
|
if (state.IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 걷기 모션이 재생 중이라면
|
||||||
agent.SetDestination(ironBall.transform.position);
|
|
||||||
agent.isStopped = false;
|
|
||||||
|
|
||||||
if (animator != null) animator.SetFloat("Speed", agent.velocity.magnitude);
|
|
||||||
|
|
||||||
float dist = Vector3.Distance(transform.position, ironBall.transform.position);
|
|
||||||
|
|
||||||
if (dist <= 3.0f && !isAttacking)
|
|
||||||
{
|
{
|
||||||
StartCoroutine(PickUpBallRoutine());
|
animator.Play(anim_Idle); // 애니메이션을 바꿀거에요 -> 대기 모션으로
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerator PickUpBallRoutine()
|
if (_timer <= 0) // 조건이 맞으면 실행할거에요 -> 공격 쿨타임이 0 이하라면
|
||||||
{
|
{
|
||||||
OnAttackStart();
|
_timer = patternInterval; // 값을 넣을거에요 -> 쿨타임을 다시 패턴 대기 시간으로
|
||||||
|
Debug.Log($"⏰ [Boss] 사거리 내 공격! 거리={distToPlayer:F1}m"); // 로그를 출력할거에요 -> 공격 시점과 거리 정보를
|
||||||
if (agent != null && agent.isOnNavMesh)
|
DecideAttack(); // 함수를 실행할거에요 -> 어떤 공격을 할지 고르는 DecideAttack을
|
||||||
{
|
|
||||||
agent.isStopped = true;
|
|
||||||
agent.velocity = Vector3.zero;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (animator != null)
|
|
||||||
{
|
|
||||||
animator.SetFloat("Speed", 0);
|
|
||||||
animator.Play("Skill_Pickup");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
yield return new WaitForSeconds(0.8f);
|
|
||||||
|
|
||||||
ironBall.transform.SetParent(handHolder);
|
|
||||||
ironBall.transform.localPosition = Vector3.zero;
|
|
||||||
ironBall.transform.localRotation = Quaternion.identity;
|
|
||||||
|
|
||||||
if (ballRb != null)
|
|
||||||
{
|
|
||||||
ballRb.isKinematic = true;
|
|
||||||
ballRb.velocity = Vector3.zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield return new WaitForSeconds(1.0f);
|
|
||||||
|
|
||||||
isWeaponless = false;
|
|
||||||
OnAttackEnd();
|
|
||||||
|
|
||||||
if (agent != null && agent.isOnNavMesh) agent.isStopped = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════
|
// ════════════════════════════════════════
|
||||||
// 🎬 전투 입장
|
// 🎬 전투 입장
|
||||||
// ════════════════════════════════════════
|
// ════════════════════════════════════════
|
||||||
|
|
||||||
public void StartBossBattle()
|
public void StartBossBattle() // 함수를 선언할거에요 -> 외부에서 전투를 시작시킬 수 있는 StartBossBattle을
|
||||||
{
|
{
|
||||||
if (isBattleStarted) return;
|
if (isBattleStarted) return; // 조건이 맞으면 중단할거에요 -> 이미 전투가 시작되었다면 중복 실행을
|
||||||
StartCoroutine(BattleStartRoutine());
|
StartCoroutine(BattleStartRoutine()); // 코루틴을 실행할거에요 -> Roar 연출 후 전투를 시작하는 BattleStartRoutine을
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerator BattleStartRoutine()
|
private IEnumerator BattleStartRoutine() // 코루틴 함수를 선언할거에요 -> Roar 연출과 전투 시작을 담당하는 BattleStartRoutine을
|
||||||
{
|
{
|
||||||
Debug.Log("🔥 보스 전투 시작! (Roar)");
|
Debug.Log("🔥 [Boss] Roar 시작!"); // 로그를 출력할거에요 -> Roar가 시작되었다는 메시지를
|
||||||
|
isPerformingAction = true; // 상태를 바꿀거에요 -> 연출 중 플래그를 참(true)으로
|
||||||
|
|
||||||
// ⭐ [핵심] 연출 중 플래그 ON → ExecuteAILogic이 아무것도 안 함
|
if (bossHealthBar != null) bossHealthBar.SetActive(true); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에 켜기를
|
||||||
isPerformingAction = true;
|
if (counterSystem != null) counterSystem.InitializeBattle(); // 조건이 맞으면 실행할거에요 -> 카운터 시스템이 있다면 전투 초기화를
|
||||||
|
|
||||||
if (bossHealthBar != null) bossHealthBar.SetActive(true);
|
if (animator != null) animator.Play(anim_Roar, 0, 0f); // 조건이 맞으면 실행할거에요 -> Roar 애니메이션을 처음부터 재생을
|
||||||
if (counterSystem != null) counterSystem.InitializeBattle();
|
|
||||||
|
|
||||||
// ⭐ [핵심] Roar 재생
|
yield return new WaitForSeconds(2.0f); // 기다릴거에요 -> Roar 애니메이션이 끝날 2초의 시간을
|
||||||
if (animator != null)
|
|
||||||
|
if (animator != null) animator.Play(anim_Idle, 0, 0f); // 조건이 맞으면 실행할거에요 -> Roar가 끝났으니 대기 모션으로 전환을
|
||||||
|
|
||||||
|
isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 중 플래그를 거짓(false)으로
|
||||||
|
isBattleStarted = true; // 상태를 바꿀거에요 -> 전투 시작 플래그를 참(true)으로
|
||||||
|
|
||||||
|
Debug.Log("⚔️ [Boss] Roar 종료 → 전투 루프 시작!"); // 로그를 출력할거에요 -> 전투 루프가 시작되었다는 메시지를
|
||||||
|
|
||||||
|
if (agent != null) // 조건이 맞으면 실행할거에요 -> 길찾기 에이전트가 존재한다면
|
||||||
{
|
{
|
||||||
animator.Play("Roar", 0, 0f);
|
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초의 시간을
|
||||||
}
|
}
|
||||||
|
|
||||||
// Roar 애니메이션 대기 (길이에 맞게 조절)
|
if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 올라갔다면
|
||||||
yield return new WaitForSeconds(2.0f);
|
|
||||||
|
|
||||||
// ⭐ [핵심] Roar 끝나면 Idle로 전환
|
|
||||||
if (animator != null)
|
|
||||||
{
|
{
|
||||||
animator.Play("Monster_Idle", 0, 0f);
|
agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 추격을 시작하라고
|
||||||
|
Debug.Log("✅ [Boss] NavMesh 연결 완료, 추격 시작"); // 로그를 출력할거에요 -> NavMesh 연결 성공 메시지를
|
||||||
}
|
}
|
||||||
|
else // 조건이 틀리면 실행할거에요 -> NavMesh에 못 올라갔다면
|
||||||
// 연출 종료 → 전투 시작
|
|
||||||
isPerformingAction = false;
|
|
||||||
isBattleStarted = true;
|
|
||||||
|
|
||||||
// NavMeshAgent 활성화
|
|
||||||
if (agent != null)
|
|
||||||
{
|
{
|
||||||
agent.enabled = true;
|
Debug.LogError("❌ [Boss] NavMesh 연결 실패! 보스 위치를 확인하세요!"); // 에러를 출력할거에요 -> 연결 실패 경고를
|
||||||
|
|
||||||
// NavMesh 위에 올라갈 때까지 잠시 대기
|
|
||||||
int retry = 0;
|
|
||||||
while (!agent.isOnNavMesh && retry < 10)
|
|
||||||
{
|
|
||||||
retry++;
|
|
||||||
yield return new WaitForSeconds(0.1f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (agent.isOnNavMesh)
|
|
||||||
{
|
|
||||||
agent.isStopped = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.Log("⚔️ 보스 전투 루프 시작!");
|
// ════════════════════════════════════════
|
||||||
|
// 🚶 이동 애니메이션 & 타겟팅
|
||||||
|
// ════════════════════════════════════════
|
||||||
|
|
||||||
|
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)
|
private void LookAtTarget(Vector3 targetPos) // 함수를 선언할거에요 -> 타겟을 부드럽게 쳐다보는 LookAtTarget을
|
||||||
{
|
{
|
||||||
Vector3 dir = targetPos - transform.position;
|
Vector3 dir = targetPos - transform.position; // 벡터를 계산할거에요 -> 내 위치에서 타겟까지의 방향을
|
||||||
dir.y = 0;
|
dir.y = 0; // 값을 바꿀거에요 -> 위아래로 고개가 꺾이지 않게 y를 0으로
|
||||||
if (dir != Vector3.zero)
|
if (dir != Vector3.zero) // 조건이 맞으면 실행할거에요 -> 방향이 0이 아니라면
|
||||||
{
|
{
|
||||||
transform.rotation = Quaternion.Slerp(
|
transform.rotation = Quaternion.Slerp( // 회전시킬거에요 -> 현재 회전에서 타겟 방향으로 부드럽게
|
||||||
transform.rotation,
|
transform.rotation, // 시작값으로 사용할거에요 -> 현재 회전값을
|
||||||
Quaternion.LookRotation(dir),
|
Quaternion.LookRotation(dir), // 목표값으로 사용할거에요 -> 타겟을 향하는 회전값을
|
||||||
Time.deltaTime * 5f
|
Time.deltaTime * 5f // 속도를 지정할거에요 -> 프레임 시간의 5배 속도로
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════
|
// ════════════════════════════════════════
|
||||||
// 🧠 AI 공격 판단
|
// 🧠 공격 판단
|
||||||
// ════════════════════════════════════════
|
// ════════════════════════════════════════
|
||||||
|
|
||||||
private void DecideAttack()
|
private void DecideAttack() // 함수를 선언할거에요 -> 어떤 공격 패턴을 쓸지 고르는 DecideAttack을
|
||||||
{
|
{
|
||||||
if (animator != null) animator.SetFloat("Speed", 0f);
|
if (target != null) LookAtTarget(target.position); // 조건이 맞으면 실행할거에요 -> 타겟이 있다면 그쪽을 바라보기를
|
||||||
|
|
||||||
// 공격 전 이동 멈춤
|
string patternName = (counterSystem != null) ? counterSystem.SelectBossPattern() : "Normal"; // 변수를 선언할거에요 -> 카운터 시스템에서 받은 패턴 이름을 patternName에
|
||||||
if (agent != null && agent.isOnNavMesh)
|
Debug.Log($"🧠 [Boss] 패턴 선택: {patternName}"); // 로그를 출력할거에요 -> 선택된 패턴 이름을
|
||||||
|
|
||||||
|
switch (patternName) // 분기할거에요 -> 받아온 패턴 이름에 따라서
|
||||||
{
|
{
|
||||||
agent.isStopped = true;
|
case "DashAttack": StartCoroutine(Pattern_DashAttack()); break; // 일치하면 실행할거에요 -> 돌진 공격을
|
||||||
agent.velocity = Vector3.zero;
|
case "Smash": StartCoroutine(Pattern_SmashAttack()); break; // 일치하면 실행할거에요 -> 내려찍기 공격을
|
||||||
}
|
case "ShieldWall": StartCoroutine(Pattern_ShieldWall()); break; // 일치하면 실행할거에요 -> 휩쓸기 공격을
|
||||||
|
default: StartCoroutine(Pattern_ThrowBall()); break; // 맞는게 없으면 실행할거에요 -> 공 던지기 공격을
|
||||||
// 플레이어를 바라봄
|
|
||||||
if (target != null) LookAtTarget(target.position);
|
|
||||||
|
|
||||||
string patternName = (counterSystem != null) ? counterSystem.SelectBossPattern() : "Normal";
|
|
||||||
|
|
||||||
Debug.Log($"🧠 보스 패턴 선택: {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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════
|
// ════════════════════════════════════════
|
||||||
// ⚔️ 공격 패턴
|
// 🏃 무기 회수
|
||||||
// ════════════════════════════════════════
|
// ════════════════════════════════════════
|
||||||
|
|
||||||
// 1. 돌진
|
private void RetrieveWeaponLogic() // 함수를 선언할거에요 -> 던진 쇠공을 주우러 가는 RetrieveWeaponLogic을
|
||||||
private IEnumerator Pattern_DashAttack()
|
|
||||||
{
|
{
|
||||||
OnAttackStart();
|
if (ironBall == null || !agent.enabled || !agent.isOnNavMesh) return; // 조건이 맞으면 중단할거에요 -> 쇠공이 없거나 길찾기가 불가능하다면
|
||||||
|
|
||||||
if (agent != null) agent.enabled = false;
|
agent.SetDestination(ironBall.transform.position); // 명령을 내릴거에요 -> 길찾기 목표를 쇠공의 위치로
|
||||||
|
agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 걸어가라고
|
||||||
|
UpdateMovementAnimation(); // 함수를 실행할거에요 -> 걸어갈 때 이동 애니메이션이 나오도록
|
||||||
|
|
||||||
if (animator != null) animator.Play("Skill_Dash_Ready", 0, 0f);
|
if (Vector3.Distance(transform.position, ironBall.transform.position) <= 3.0f && !isAttacking) // 조건이 맞으면 실행할거에요 -> 쇠공과 3m 이내이고 공격 중이 아니라면
|
||||||
yield return new WaitForSeconds(0.5f);
|
StartCoroutine(PickUpBallRoutine()); // 코루틴을 실행할거에요 -> 바닥의 쇠공을 줍는 PickUpBallRoutine을
|
||||||
|
|
||||||
// 돌진
|
|
||||||
if (rb != null)
|
|
||||||
{
|
|
||||||
rb.isKinematic = false;
|
|
||||||
rb.velocity = transform.forward * 20f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (animator != null) animator.Play("Skill_Dash_Go", 0, 0f);
|
private IEnumerator PickUpBallRoutine() // 코루틴 함수를 선언할거에요 -> 쇠공 줍기 행동인 PickUpBallRoutine을
|
||||||
yield return new WaitForSeconds(1.0f);
|
|
||||||
|
|
||||||
// 정지
|
|
||||||
if (rb != null)
|
|
||||||
{
|
{
|
||||||
rb.velocity = Vector3.zero;
|
OnAttackStart(); // 상태를 켤거에요 -> 다른 행동과 겹치지 않게 공격 플래그를
|
||||||
rb.isKinematic = true;
|
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초를
|
||||||
|
|
||||||
// NavMeshAgent 복구
|
ironBall.transform.SetParent(handHolder); // 부모를 바꿀거에요 -> 쇠공을 보스의 손 밑으로
|
||||||
if (agent != null && isBattleStarted)
|
ironBall.transform.localPosition = Vector3.zero; // 위치를 바꿀거에요 -> 쇠공의 위치를 손의 정중앙으로
|
||||||
{
|
ironBall.transform.localRotation = Quaternion.identity; // 회전을 바꿀거에요 -> 쇠공의 회전을 기본값으로
|
||||||
agent.enabled = true;
|
if (ballRb != null) { ballRb.isKinematic = true; ballRb.velocity = Vector3.zero; } // 조건이 맞으면 실행할거에요 -> 쇠공의 물리를 끄고 속도를 0으로
|
||||||
// NavMesh 복귀 대기
|
|
||||||
int retry = 0;
|
|
||||||
while (agent != null && !agent.isOnNavMesh && retry < 10)
|
|
||||||
{
|
|
||||||
retry++;
|
|
||||||
yield return new WaitForSeconds(0.1f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OnAttackEnd();
|
yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 일어서는 애니메이션이 끝날 1초를
|
||||||
}
|
isWeaponless = false; // 상태를 바꿀거에요 -> 무기를 다시 쥐었으므로 맨손 상태를 거짓으로
|
||||||
|
OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 처리를
|
||||||
// 2. 스매시
|
if (agent != null && agent.isOnNavMesh) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 다시 추격을 위해 이동 정지 해제를
|
||||||
private IEnumerator Pattern_SmashAttack()
|
|
||||||
{
|
|
||||||
OnAttackStart();
|
|
||||||
|
|
||||||
if (animator != null) animator.Play("Skill_Smash_Impact", 0, 0f);
|
|
||||||
yield return new WaitForSeconds(1.2f);
|
|
||||||
|
|
||||||
if (animator != null) animator.Play("Skill_Smash_Impact", 0, 0f);
|
|
||||||
yield return new WaitForSeconds(1.0f);
|
|
||||||
|
|
||||||
OnAttackEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 쓸기
|
|
||||||
private IEnumerator Pattern_ShieldWall()
|
|
||||||
{
|
|
||||||
OnAttackStart();
|
|
||||||
|
|
||||||
if (animator != null) animator.Play("Skill_Sweep", 0, 0f);
|
|
||||||
yield return new WaitForSeconds(2.0f);
|
|
||||||
|
|
||||||
OnAttackEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 공 던지기
|
|
||||||
private IEnumerator Pattern_ThrowBall()
|
|
||||||
{
|
|
||||||
OnAttackStart();
|
|
||||||
|
|
||||||
if (animator != null) animator.Play("Attack_Throw", 0, 0f);
|
|
||||||
yield return new WaitForSeconds(0.5f);
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
yield return new WaitForSeconds(1.0f);
|
|
||||||
OnAttackEnd();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════
|
// ════════════════════════════════════════
|
||||||
// ⭐ [신규] 피격 시 부모 클래스 StopAllCoroutines 대응
|
// ✅ 공격 시작/종료 처리 (핵심)
|
||||||
// ════════════════════════════════════════
|
// ════════════════════════════════════════
|
||||||
|
|
||||||
/// <summary>
|
public override void OnAttackStart() // 함수를 덮어씌워 실행할거에요 -> 공격 시작 처리를 하는 OnAttackStart를
|
||||||
/// 부모의 StartHit()이 StopAllCoroutines()를 호출하므로,
|
|
||||||
/// 보스 전용 상태를 여기서 복구합니다.
|
|
||||||
/// </summary>
|
|
||||||
protected override void OnStartHit()
|
|
||||||
{
|
{
|
||||||
isPerformingAction = false; // 연출 중이었으면 해제
|
isAttacking = true; // 상태를 바꿀거에요 -> 공격 중임을 참으로
|
||||||
|
isPerformingAction = true; // 상태를 바꿀거에요 -> 다른 행동 불가 상태로
|
||||||
// 공격 중이었으면 해제
|
if (agent != null) agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고
|
||||||
if (isAttacking)
|
|
||||||
{
|
|
||||||
isAttacking = false;
|
|
||||||
if (myWeapon != null) myWeapon.DisableHitBox();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 돌진 중 물리 복구
|
public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 처리를 하는 OnAttackEnd를
|
||||||
if (rb != null && !rb.isKinematic)
|
|
||||||
{
|
{
|
||||||
rb.velocity = Vector3.zero;
|
StartCoroutine(RecoverRoutine()); // 코루틴을 시작할거에요 -> 후딜레이(회복) 처리를 위한 RecoverRoutine을
|
||||||
rb.isKinematic = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private IEnumerator RecoverRoutine() // 코루틴 함수를 선언할거에요 -> 공격 후 대기 시간을 갖는 RecoverRoutine을
|
||||||
/// 피격 종료 후 NavMeshAgent 복구
|
|
||||||
/// </summary>
|
|
||||||
public override void OnHitEnd()
|
|
||||||
{
|
{
|
||||||
base.OnHitEnd();
|
isAttacking = false; // 상태를 바꿀거에요 -> 공격이 끝났으므로 공격 중 상태를 거짓으로
|
||||||
|
isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참으로 (이때는 추격 안 함)
|
||||||
|
|
||||||
// 전투 중이면 Agent 다시 활성화
|
if (animator != null) animator.Play(anim_Idle); // 조건이 맞으면 실행할거에요 -> 대기 모션으로 전환을
|
||||||
if (isBattleStarted && agent != null && !agent.enabled)
|
|
||||||
{
|
// 딜레이 시간 (여기서 patternInterval 만큼 멍때림)
|
||||||
agent.enabled = true;
|
yield return new WaitForSeconds(patternInterval); // 기다릴거에요 -> 설정된 패턴 간격 시간만큼
|
||||||
}
|
|
||||||
|
isResting = false; // 상태를 바꿀거에요 -> 휴식이 끝났으므로 거짓으로
|
||||||
|
isPerformingAction = false; // 상태를 바꿀거에요 -> 이제 다시 다른 행동이 가능하도록 거짓으로
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════
|
// ════════════════════════════════════════
|
||||||
// ⭐ [신규] 사망 처리
|
// ⚔️ 공격 패턴들
|
||||||
// ════════════════════════════════════════
|
// ════════════════════════════════════════
|
||||||
|
|
||||||
protected override void OnDie()
|
// 패턴 1: 돌진 공격
|
||||||
|
private IEnumerator Pattern_DashAttack() // 코루틴 함수를 선언할거에요 -> 돌진 패턴인 Pattern_DashAttack을
|
||||||
{
|
{
|
||||||
isBattleStarted = false;
|
OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를
|
||||||
isPerformingAction = false;
|
if (agent != null) agent.enabled = false; // 조건이 맞으면 실행할거에요 -> 돌진 시 물리력을 위해 길찾기를 끄기로
|
||||||
isWeaponless = false;
|
|
||||||
|
|
||||||
// 물리 정리
|
if (animator != null) animator.Play(anim_DashReady, 0, 0f); // 조건이 맞으면 실행할거에요 -> 돌진 준비 모션을
|
||||||
if (rb != null)
|
yield return new WaitForSeconds(0.5f); // 기다릴거에요 -> 준비 포즈를 취할 0.5초를
|
||||||
{
|
|
||||||
rb.velocity = Vector3.zero;
|
if (rb != null) { rb.isKinematic = false; rb.velocity = transform.forward * 20f; } // 조건이 맞으면 실행할거에요 -> 물리를 켜고 앞으로 속도 20으로 돌진을
|
||||||
rb.isKinematic = true;
|
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: 내려찍기
|
||||||
if (bossHealthBar != null) bossHealthBar.SetActive(false);
|
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>"); // 로그를 출력할거에요 -> 빨간 글씨로 처치 완료 메시지를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,29 +1,29 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
public class BossZoneTrigger : MonoBehaviour
|
public class BossZoneTrigger : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossZoneTrigger를
|
||||||
{
|
{
|
||||||
[SerializeField] private NorcielBoss boss; // 깨울 보스 연결
|
[SerializeField] private NorcielBoss boss; // 변수를 선언할거에요 -> 깨울 보스 스크립트(NorcielBoss)를 boss에
|
||||||
[SerializeField] private GameObject fogWall; // 보스 방 입구 막는 안개 (선택)
|
[SerializeField] private GameObject fogWall; // 변수를 선언할거에요 -> 입구를 막을 안개벽 오브젝트를 fogWall에
|
||||||
|
|
||||||
private bool hasTriggered = false;
|
private bool hasTriggered = false; // 변수를 초기화할거에요 -> 이미 발동했는지 여부를 거짓(false)으로
|
||||||
|
|
||||||
private void OnTriggerEnter(Collider other)
|
private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 무언가가 트리거에 닿았을 때 OnTriggerEnter를
|
||||||
{
|
{
|
||||||
// 이미 발동했거나, 플레이어가 아니면 무시
|
// 이미 발동했거나, 플레이어가 아니면 무시
|
||||||
if (hasTriggered) return;
|
if (hasTriggered) return; // 조건이 맞으면 중단할거에요 -> 이미 발동했다면(hasTriggered가 참이면)
|
||||||
|
|
||||||
if (other.CompareTag("Player"))
|
if (other.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 닿은 물체의 태그가 "Player"라면
|
||||||
{
|
{
|
||||||
hasTriggered = true;
|
hasTriggered = true; // 상태를 바꿀거에요 -> 발동 상태를 참(true)으로
|
||||||
|
|
||||||
// 1. 보스 깨우기
|
// 1. 보스 깨우기
|
||||||
if (boss != null) boss.StartBossBattle();
|
if (boss != null) boss.StartBossBattle(); // 조건이 맞으면 실행할거에요 -> 보스가 연결되어 있다면 전투 시작 함수(StartBossBattle)를
|
||||||
|
|
||||||
// 2. 도망 못 가게 입구 막기 (선택)
|
// 2. 도망 못 가게 입구 막기 (선택)
|
||||||
if (fogWall != null) fogWall.SetActive(true);
|
if (fogWall != null) fogWall.SetActive(true); // 조건이 맞으면 실행할거에요 -> 안개벽이 연결되어 있다면 활성화(true)를
|
||||||
|
|
||||||
// 3. 이 트리거는 할 일 다 했으니 끄기
|
// 3. 이 트리거는 할 일 다 했으니 끄기
|
||||||
// gameObject.SetActive(false); // 바로 끄면 안개도 꺼질 수 있으니 주의
|
// gameObject.SetActive(false); // 바로 끄면 안개도 꺼질 수 있으니 주의
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
/// <summary>
|
/// <summary> // 요약 설명을 적을거에요 -> 아래 enum이 뭔지
|
||||||
/// 보스 카운터 패턴 타입 열거형
|
/// 보스 카운터 패턴 타입 열거형 // 설명할거에요 -> 보스가 어떤 카운터 패턴을 쓰는지 구분하는 타입
|
||||||
/// </summary>
|
/// </summary> // 요약 설명을 끝낼거에요 -> 주석 블록을
|
||||||
public enum CounterType
|
public enum CounterType // 열거형을 선언할거에요 -> CounterType이라는 이름의 enum을
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> CounterType 범위를
|
||||||
None,
|
None, // 값을 정의할거에요 -> 카운터 없음 상태를
|
||||||
Dodge, // 회피 남발 카운터
|
Dodge, // 값을 정의할거에요 -> 회피 남발 카운터 타입을
|
||||||
Aim, // 정조준 과다 카운터
|
Aim, // 값을 정의할거에요 -> 정조준 과다 카운터 타입을
|
||||||
Pierce // 관통 화살 편중 카운터
|
Pierce // 값을 정의할거에요 -> 관통 화살 편중 카운터 타입을
|
||||||
}
|
} // 코드 블록을 끝낼거에요 -> CounterType을
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 하이브리드 몬스터 스포너 (생성은 스포너 기준, 생존은 몬스터 기준)
|
/// 하이브리드 몬스터 스포너 (생성은 스포너 기준, 생존은 몬스터 기준)
|
||||||
|
|
@ -6,52 +6,52 @@
|
||||||
/// - Life & Hibernate: 몬스터 기준 거리로 최적화
|
/// - Life & Hibernate: 몬스터 기준 거리로 최적화
|
||||||
/// - Wake Up: 범위 안으로 복귀 시 재활성화
|
/// - Wake Up: 범위 안으로 복귀 시 재활성화
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MonsterSpawner : MonoBehaviour
|
public class MonsterSpawner : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 MonsterSpawner를
|
||||||
{
|
{
|
||||||
[Header("=== 생성 설정 (Birth - 스포너 기준) ===")]
|
[Header("=== 생성 설정 (Birth - 스포너 기준) ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 생성 설정 (Birth - 스포너 기준) === 을
|
||||||
[Tooltip("스포너로부터 이 거리 안에 플레이어가 들어오면 몬스터 생성")]
|
[Tooltip("스포너로부터 이 거리 안에 플레이어가 들어오면 몬스터 생성")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[SerializeField] private float spawnRange = 25f;
|
[SerializeField] private float spawnRange = 25f; // 변수를 선언할거에요 -> 몬스터 생성 거리(25.0)를 spawnRange에
|
||||||
|
|
||||||
[Tooltip("몬스터가 죽은 후 재생성까지 대기 시간")]
|
[Tooltip("몬스터가 죽은 후 재생성까지 대기 시간")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[SerializeField] private float respawnCooldown = 3f;
|
[SerializeField] private float respawnCooldown = 3f; // 변수를 선언할거에요 -> 재소환 대기시간(3초)을 respawnCooldown에
|
||||||
|
|
||||||
[Tooltip("오브젝트 풀에서 가져올 몬스터 태그")]
|
[Tooltip("오브젝트 풀에서 가져올 몬스터 태그")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[SerializeField] private string mobTag = "NormalMob";
|
[SerializeField] private string mobTag = "NormalMob"; // 변수를 선언할거에요 -> 소환할 몬스터의 태그 이름("NormalMob")을 mobTag에
|
||||||
|
|
||||||
[Header("=== 최적화 설정 (Life & Hibernate - 몬스터 기준) ===")]
|
[Header("=== 최적화 설정 (Life & Hibernate - 몬스터 기준) ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 최적화 설정 (Life & Hibernate - 몬스터 기준) === 을
|
||||||
[Tooltip("몬스터 본체로부터 이 거리를 벗어나면 강제 동면 (전투 중이어도!)")]
|
[Tooltip("몬스터 본체로부터 이 거리를 벗어나면 강제 동면 (전투 중이어도!)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[SerializeField] private float optimizationRange = 60f;
|
[SerializeField] private float optimizationRange = 60f; // 변수를 선언할거에요 -> 몬스터 최적화(동면) 거리(60.0)를 optimizationRange에
|
||||||
|
|
||||||
// 내부 상태
|
// 내부 상태
|
||||||
private GameObject _myMonster;
|
private GameObject _myMonster; // 변수를 선언할거에요 -> 소환된 몬스터 게임오브젝트를 담을 _myMonster를
|
||||||
private MonsterClass _monsterScript;
|
private MonsterClass _monsterScript; // 변수를 선언할거에요 -> 몬스터의 스크립트(MonsterClass)를 담을 _monsterScript를
|
||||||
private Transform _player;
|
private Transform _player; // 변수를 선언할거에요 -> 플레이어의 위치 정보를 담을 _player를
|
||||||
private float _nextSpawnTime;
|
private float _nextSpawnTime; // 변수를 선언할거에요 -> 다음 소환 가능 시간을 저장할 _nextSpawnTime을
|
||||||
|
|
||||||
private void Start()
|
private void Start() // 함수를 실행할거에요 -> 게임 시작 시 호출되는 Start를
|
||||||
{
|
{
|
||||||
FindPlayer();
|
FindPlayer(); // 함수를 실행할거에요 -> 플레이어를 찾는 FindPlayer를
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FindPlayer()
|
private void FindPlayer() // 함수를 선언할거에요 -> 플레이어를 찾아 변수에 저장하는 FindPlayer를
|
||||||
{
|
{
|
||||||
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
|
GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); // 오브젝트를 찾을거에요 -> "Player" 태그가 붙은 오브젝트를 playerObj에
|
||||||
if (playerObj != null) _player = playerObj.transform;
|
if (playerObj != null) _player = playerObj.transform; // 조건이 맞으면 실행할거에요 -> 플레이어를 찾았다면 그 위치 정보를 _player에 저장
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를
|
||||||
{
|
{
|
||||||
if (_player == null) { FindPlayer(); return; }
|
if (_player == null) { FindPlayer(); return; } // 조건이 맞으면 실행할거에요 -> 플레이어 정보가 없다면 다시 찾고 이번 프레임은 중단(return)
|
||||||
|
|
||||||
// 몬스터가 없거나 죽었으면 -> Birth
|
// 몬스터가 없거나 죽었으면 -> Birth
|
||||||
if (_myMonster == null || (_monsterScript != null && _monsterScript.IsDead))
|
if (_myMonster == null || (_monsterScript != null && _monsterScript.IsDead)) // 조건이 맞으면 실행할거에요 -> 몬스터가 없거나, 몬스터 스크립트가 있는데 죽은 상태라면
|
||||||
{
|
{
|
||||||
HandleBirth();
|
HandleBirth(); // 함수를 실행할거에요 -> 몬스터 생성 로직인 HandleBirth를
|
||||||
}
|
}
|
||||||
// 몬스터가 살아있으면 -> Life & Hibernate
|
// 몬스터가 살아있으면 -> Life & Hibernate
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 몬스터가 살아서 활동 중이라면
|
||||||
{
|
{
|
||||||
HandleLifeAndHibernate();
|
HandleLifeAndHibernate(); // 함수를 실행할거에요 -> 생존 및 최적화(동면) 로직인 HandleLifeAndHibernate를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,25 +59,25 @@ public class MonsterSpawner : MonoBehaviour
|
||||||
// 🐣 BIRTH (생성) - 스포너 기준
|
// 🐣 BIRTH (생성) - 스포너 기준
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
private void HandleBirth()
|
private void HandleBirth() // 함수를 선언할거에요 -> 몬스터 생성 조건을 확인하는 HandleBirth를
|
||||||
{
|
{
|
||||||
// ⭐ 스포너 <-> 플레이어 거리
|
// ⭐ 스포너 <-> 플레이어 거리
|
||||||
float distanceFromSpawner = Vector3.Distance(transform.position, _player.position);
|
float distanceFromSpawner = Vector3.Distance(transform.position, _player.position); // 값을 계산할거에요 -> 스포너와 플레이어 사이의 거리를 distanceFromSpawner에
|
||||||
|
|
||||||
if (distanceFromSpawner <= spawnRange && Time.time >= _nextSpawnTime)
|
if (distanceFromSpawner <= spawnRange && Time.time >= _nextSpawnTime) // 조건이 맞으면 실행할거에요 -> 거리가 생성 범위 이내이고, 쿨타임 시간이 지났다면
|
||||||
{
|
{
|
||||||
SpawnMonster();
|
SpawnMonster(); // 함수를 실행할거에요 -> 실제 몬스터를 소환하는 SpawnMonster를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SpawnMonster()
|
private void SpawnMonster() // 함수를 선언할거에요 -> 오브젝트 풀에서 몬스터를 가져오는 SpawnMonster를
|
||||||
{
|
{
|
||||||
_myMonster = GenericObjectPool.Instance.SpawnFromPool(mobTag, transform.position, transform.rotation);
|
_myMonster = GenericObjectPool.Instance.SpawnFromPool(mobTag, transform.position, transform.rotation); // 오브젝트를 가져올거에요 -> 풀(Pool)에서 mobTag에 맞는 몬스터를 스포너 위치에 소환해서 _myMonster에
|
||||||
|
|
||||||
if (_myMonster != null)
|
if (_myMonster != null) // 조건이 맞으면 실행할거에요 -> 몬스터 소환에 성공했다면
|
||||||
{
|
{
|
||||||
_monsterScript = _myMonster.GetComponent<MonsterClass>();
|
_monsterScript = _myMonster.GetComponent<MonsterClass>(); // 컴포넌트를 가져올거에요 -> 몬스터의 핵심 스크립트(MonsterClass)를 _monsterScript에
|
||||||
if (_monsterScript != null) _monsterScript.ResetStats();
|
if (_monsterScript != null) _monsterScript.ResetStats(); // 조건이 맞으면 실행할거에요 -> 스크립트가 있다면 몬스터의 상태(체력 등)를 초기화(ResetStats)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,43 +85,43 @@ public class MonsterSpawner : MonoBehaviour
|
||||||
// 💤 LIFE & HIBERNATE (생존/동면) - 몬스터 기준
|
// 💤 LIFE & HIBERNATE (생존/동면) - 몬스터 기준
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
private void HandleLifeAndHibernate()
|
private void HandleLifeAndHibernate() // 함수를 선언할거에요 -> 몬스터의 활동 및 최적화를 관리하는 HandleLifeAndHibernate를
|
||||||
{
|
{
|
||||||
if (_myMonster == null) { _monsterScript = null; return; }
|
if (_myMonster == null) { _monsterScript = null; return; } // 조건이 맞으면 실행할거에요 -> 몬스터 오브젝트가 사라졌다면 스크립트 변수도 비우고 중단(return)
|
||||||
|
|
||||||
// ⭐ 몬스터 본체 <-> 플레이어 거리
|
// ⭐ 몬스터 본체 <-> 플레이어 거리
|
||||||
float distanceFromMonster = Vector3.Distance(_myMonster.transform.position, _player.position);
|
float distanceFromMonster = Vector3.Distance(_myMonster.transform.position, _player.position); // 값을 계산할거에요 -> 몬스터와 플레이어 사이의 거리를 distanceFromMonster에
|
||||||
|
|
||||||
if (distanceFromMonster > optimizationRange)
|
if (distanceFromMonster > optimizationRange) // 조건이 맞으면 실행할거에요 -> 몬스터와 플레이어 거리가 최적화 범위(optimizationRange)보다 멀다면
|
||||||
{
|
{
|
||||||
// 😴 HIBERNATE (동면)
|
// 😴 HIBERNATE (동면)
|
||||||
HibernateMonster();
|
HibernateMonster(); // 함수를 실행할거에요 -> 몬스터를 잠재우는 HibernateMonster를
|
||||||
}
|
}
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 거리가 가깝다면
|
||||||
{
|
{
|
||||||
// 👁 WAKE UP (기상)
|
// 👁 WAKE UP (기상)
|
||||||
WakeUpMonster();
|
WakeUpMonster(); // 함수를 실행할거에요 -> 몬스터를 깨우는 WakeUpMonster를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HibernateMonster()
|
private void HibernateMonster() // 함수를 선언할거에요 -> 몬스터를 비활성화하는 HibernateMonster를
|
||||||
{
|
{
|
||||||
if (_myMonster.activeSelf)
|
if (_myMonster.activeSelf) // 조건이 맞으면 실행할거에요 -> 몬스터가 현재 켜져 있는(Active) 상태라면
|
||||||
{
|
{
|
||||||
_myMonster.SetActive(false);
|
_myMonster.SetActive(false); // 기능을 끌거에요 -> 몬스터 오브젝트를 비활성화(false)로
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WakeUpMonster()
|
private void WakeUpMonster() // 함수를 선언할거에요 -> 몬스터를 다시 활성화하는 WakeUpMonster를
|
||||||
{
|
{
|
||||||
if (!_myMonster.activeSelf)
|
if (!_myMonster.activeSelf) // 조건이 맞으면 실행할거에요 -> 몬스터가 현재 꺼져 있는(Inactive) 상태라면
|
||||||
{
|
{
|
||||||
_myMonster.SetActive(true);
|
_myMonster.SetActive(true); // 기능을 켤거에요 -> 몬스터 오브젝트를 활성화(true)로
|
||||||
|
|
||||||
if (_monsterScript != null)
|
if (_monsterScript != null) // 조건이 맞으면 실행할거에요 -> 몬스터 스크립트가 존재한다면
|
||||||
{
|
{
|
||||||
// ⭐ AI 복구 (전투 중이었다면 추적 재개)
|
// ⭐ AI 복구 (전투 중이었다면 추적 재개)
|
||||||
_monsterScript.Reactivate();
|
_monsterScript.Reactivate(); // 함수를 실행할거에요 -> 몬스터의 AI를 다시 작동시키는 Reactivate를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -133,31 +133,31 @@ public class MonsterSpawner : MonoBehaviour
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 몬스터가 죽었을 때 호출 (쿨타임 적용)
|
/// 몬스터가 죽었을 때 호출 (쿨타임 적용)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void NotifyMonsterDead()
|
public void NotifyMonsterDead() // 함수를 선언할거에요 -> 외부에서 몬스터 사망을 알릴 때 쓰는 NotifyMonsterDead를
|
||||||
{
|
{
|
||||||
// 쿨타임 계산: 현재 시간 + 대기 시간
|
// 쿨타임 계산: 현재 시간 + 대기 시간
|
||||||
_nextSpawnTime = Time.time + respawnCooldown;
|
_nextSpawnTime = Time.time + respawnCooldown; // 값을 저장할거에요 -> 현재 시간에 쿨타임을 더해서 다음 소환 시간(_nextSpawnTime)으로
|
||||||
|
|
||||||
// 몬스터 참조를 비워서 Update에서 다시 HandleBirth()로 넘어가게 함
|
// 몬스터 참조를 비워서 Update에서 다시 HandleBirth()로 넘어가게 함
|
||||||
_myMonster = null;
|
_myMonster = null; // 값을 비울거에요 -> 소환된 몬스터 변수를 null로
|
||||||
_monsterScript = null;
|
_monsterScript = null; // 값을 비울거에요 -> 몬스터 스크립트 변수를 null로
|
||||||
}
|
}
|
||||||
|
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
// Gizmos
|
// Gizmos
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
private void OnDrawGizmosSelected()
|
private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 에디터에서 오브젝트 선택 시 기즈모를 그리는 OnDrawGizmosSelected를
|
||||||
{
|
{
|
||||||
// 스포너 생성 범위 (빨간색)
|
// 스포너 생성 범위 (빨간색)
|
||||||
Gizmos.color = Color.red;
|
Gizmos.color = Color.red; // 색상을 설정할거에요 -> 기즈모 색을 빨간색으로
|
||||||
Gizmos.DrawWireSphere(transform.position, spawnRange);
|
Gizmos.DrawWireSphere(transform.position, spawnRange); // 그림을 그릴거에요 -> 스포너 위치에 생성 범위(spawnRange) 크기의 원을
|
||||||
|
|
||||||
// 몬스터 최적화 범위 (파란색)
|
// 몬스터 최적화 범위 (파란색)
|
||||||
if (_myMonster != null)
|
if (_myMonster != null) // 조건이 맞으면 실행할거에요 -> 소환된 몬스터가 있다면
|
||||||
{
|
{
|
||||||
Gizmos.color = Color.blue;
|
Gizmos.color = Color.blue; // 색상을 설정할거에요 -> 기즈모 색을 파란색으로
|
||||||
Gizmos.DrawWireSphere(_myMonster.transform.position, optimizationRange);
|
Gizmos.DrawWireSphere(_myMonster.transform.position, optimizationRange); // 그림을 그릴거에요 -> 몬스터 위치에 최적화 범위(optimizationRange) 크기의 원을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
public class MobUpdateManager : MonoBehaviour
|
public class MobUpdateManager : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 MobUpdateManager를
|
||||||
{
|
{
|
||||||
public static MobUpdateManager Instance;
|
public static MobUpdateManager Instance; // 변수를 선언할거에요 -> 어디서든 접근 가능한 싱글톤 인스턴스 Instance를
|
||||||
// ⭐ Monster -> MonsterClass로 수정하여 에러 해결
|
// ⭐ Monster -> MonsterClass로 수정하여 에러 해결
|
||||||
private List<MonsterClass> _activeMobs = new List<MonsterClass>();
|
private List<MonsterClass> _activeMobs = new List<MonsterClass>(); // 리스트를 초기화할거에요 -> 활성화된 몬스터들을 담을 _activeMobs를
|
||||||
|
|
||||||
private void Awake() => Instance = this;
|
private void Awake() => Instance = this; // 함수를 실행할거에요 -> 스크립트 시작 시 내 자신(this)을 Instance에 넣기를
|
||||||
|
|
||||||
// ⭐ 매개변수 타입도 MonsterClass로 변경
|
// ⭐ 매개변수 타입도 MonsterClass로 변경
|
||||||
public void RegisterMob(MonsterClass mob) => _activeMobs.Add(mob);
|
public void RegisterMob(MonsterClass mob) => _activeMobs.Add(mob); // 함수를 선언할거에요 -> 몬스터를 리스트에 추가하는 RegisterMob을
|
||||||
public void UnregisterMob(MonsterClass mob) => _activeMobs.Remove(mob);
|
public void UnregisterMob(MonsterClass mob) => _activeMobs.Remove(mob); // 함수를 선언할거에요 -> 몬스터를 리스트에서 제거하는 UnregisterMob을
|
||||||
|
|
||||||
private void Update()
|
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를
|
||||||
{
|
{
|
||||||
for (int i = 0; i < _activeMobs.Count; i++)
|
for (int i = 0; i < _activeMobs.Count; i++) // 반복할거에요 -> 리스트에 있는 모든 몬스터 개수만큼
|
||||||
{
|
{
|
||||||
// 리스트를 돌며 카메라 최적화 로직이 담긴 OnManagedUpdate 호출
|
// 리스트를 돌며 카메라 최적화 로직이 담긴 OnManagedUpdate 호출
|
||||||
if (_activeMobs[i] != null) _activeMobs[i].OnManagedUpdate();
|
if (_activeMobs[i] != null) _activeMobs[i].OnManagedUpdate(); // 조건이 맞으면 실행할거에요 -> 몬스터가 존재한다면 관리 업데이트 함수(OnManagedUpdate)를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,35 +1,37 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
|
||||||
using System; // ⭐ Action 사용을 위해 필요
|
using System; // Action 이벤트를 쓰기 위해 불러올거에요 -> System을
|
||||||
|
|
||||||
public class TrainingDummy : MonoBehaviour, IDamageable
|
public class TrainingDummy : MonoBehaviour, IDamageable // 클래스를 선언할거에요 -> 더미 오브젝트가 데미지를 받게 만들 TrainingDummy를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> TrainingDummy 범위를
|
||||||
[Header("더미 설정")]
|
|
||||||
[SerializeField] private float maxHP = 9999f;
|
|
||||||
private float _currentHP;
|
|
||||||
|
|
||||||
private Animator _animator;
|
[Header("더미 설정")] // 인스펙터에 제목을 표시할거에요 -> 더미 설정을
|
||||||
|
[SerializeField] private float maxHP = 9999f; // 변수를 선언할거에요 -> 최대 체력을 9999로 설정하는 maxHP를
|
||||||
|
private float _currentHP; // 변수를 선언할거에요 -> 현재 체력을 저장할 _currentHP를
|
||||||
|
|
||||||
// ⭐ UI가 이 신호를 듣고 게이지를 줄입니다.
|
private Animator _animator; // 변수를 선언할거에요 -> 애니메이터 컴포넌트를 담을 _animator를
|
||||||
public event Action<float, float> OnHealthChanged;
|
|
||||||
|
|
||||||
private void Start()
|
// ⭐ UI가 이 신호를 듣고 게이지를 줄입니다. // 설명을 적을거에요 -> UI 업데이트용 이벤트임을
|
||||||
{
|
public event Action<float, float> OnHealthChanged; // 이벤트를 선언할거에요 -> (현재체력, 최대체력) 알림을 보내는 OnHealthChanged를
|
||||||
_currentHP = maxHP;
|
|
||||||
_animator = GetComponent<Animator>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void TakeDamage(float amount)
|
private void Start() // 함수를 선언할거에요 -> 시작할 때 1번 실행되는 Start를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> Start 범위를
|
||||||
_currentHP -= amount;
|
_currentHP = maxHP; // 값을 넣을거에요 -> 현재 체력을 최대 체력으로 초기화
|
||||||
|
_animator = GetComponent<Animator>(); // 컴포넌트를 가져올거에요 -> Animator를 찾아 _animator에
|
||||||
|
} // 코드 블록을 끝낼거에요 -> Start를
|
||||||
|
|
||||||
// ⭐ UI에 현재 체력과 최대 체력 정보를 보냅니다.
|
public void TakeDamage(float amount) // 함수를 선언할거에요 -> 외부에서 데미지를 주면 호출되는 TakeDamage를
|
||||||
OnHealthChanged?.Invoke(_currentHP, maxHP);
|
{ // 코드 블록을 시작할거에요 -> TakeDamage 범위를
|
||||||
|
_currentHP -= amount; // 값을 뺄거에요 -> 현재 체력에서 데미지만큼 감소
|
||||||
|
|
||||||
Debug.Log($"<color=red>[더미]</color> 피격! 데미지: {amount} | 남은 체력: {_currentHP}");
|
// ⭐ UI에 현재 체력과 최대 체력 정보를 보냅니다. // 설명을 적을거에요 -> UI 갱신 트리거임을
|
||||||
|
OnHealthChanged?.Invoke(_currentHP, maxHP); // 이벤트를 호출할거에요 -> UI에게 체력 변화 전달
|
||||||
|
|
||||||
if (_animator != null)
|
Debug.Log($"<color=red>[더미]</color> 피격! 데미지: {amount} | 남은 체력: {_currentHP}"); // 로그를 찍을거에요 -> 데미지/남은 체력 확인용
|
||||||
{
|
|
||||||
_animator.Play("Hit", 0, 0f);
|
if (_animator != null) // 조건을 검사할거에요 -> 애니메이터가 있는지
|
||||||
}
|
{ // 코드 블록을 시작할거에요 -> 애니메이션 재생 처리
|
||||||
}
|
_animator.Play("Hit", 0, 0f); // 애니메이션을 재생할거에요 -> Hit 상태를 0번 레이어에서 처음부터
|
||||||
}
|
} // 코드 블록을 끝낼거에요 -> 애니메이션 처리
|
||||||
|
} // 코드 블록을 끝낼거에요 -> TakeDamage를
|
||||||
|
|
||||||
|
} // 코드 블록을 끝낼거에요 -> TrainingDummy를
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
|
||||||
|
|
||||||
public class HealthPotion : MonoBehaviour
|
public class HealthPotion : MonoBehaviour // 클래스를 선언할거에요 -> 체력 포션 아이템인 HealthPotion을
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> HealthPotion 범위를
|
||||||
[Header("--- 포션 설정 ---")]
|
|
||||||
[SerializeField] private float healAmount = 30f; // 회복량
|
|
||||||
|
|
||||||
// PlayerInteraction에서 호출할 함수
|
[Header("--- 포션 설정 ---")] // 인스펙터에 제목을 표시할거에요 -> --- 포션 설정 --- 을
|
||||||
public void Use(PlayerHealth target)
|
[SerializeField] private float healAmount = 30f; // 변수를 선언할거에요 -> 회복량(30)을 healAmount에
|
||||||
{
|
|
||||||
if (target == null) return;
|
|
||||||
|
|
||||||
target.Heal(healAmount); // 플레이어 체력 회복
|
// PlayerInteraction에서 호출할 함수 // 설명을 적을거에요 -> 상호작용 스크립트가 이걸 실행한다는 걸
|
||||||
Debug.Log($"<color=green>[Potion]</color> 체력 {healAmount} 회복!");
|
public void Use(PlayerHealth target) // 함수를 선언할거에요 -> 대상 플레이어를 회복시키는 Use를
|
||||||
|
{ // 코드 블록을 시작할거에요 -> Use 범위를
|
||||||
|
if (target == null) return; // 조건이 맞으면 종료할거에요 -> 대상이 없으면 아무것도 안 함
|
||||||
|
|
||||||
Destroy(gameObject); // 사용 후 아이템 삭제
|
target.Heal(healAmount); // 함수를 실행할거에요 -> 플레이어 체력을 healAmount 만큼 회복
|
||||||
}
|
Debug.Log($"<color=green>[Potion]</color> 체력 {healAmount} 회복!"); // 로그를 찍을거에요 -> 회복 로그 출력
|
||||||
}
|
|
||||||
|
Destroy(gameObject); // 오브젝트를 삭제할거에요 -> 사용한 포션 아이템 제거
|
||||||
|
} // 코드 블록을 끝낼거에요 -> Use를
|
||||||
|
|
||||||
|
} // 코드 블록을 끝낼거에요 -> HealthPotion을
|
||||||
|
|
@ -1,63 +1,65 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
|
||||||
|
|
||||||
public class ItemHighlight : MonoBehaviour
|
public class ItemHighlight : MonoBehaviour // 클래스를 선언할거에요 -> 아이템 하이라이트(발광 깜빡임) 담당 ItemHighlight를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> ItemHighlight 범위를
|
||||||
[Header("--- 참조 ---")]
|
|
||||||
[SerializeField] private MeshRenderer weaponRenderer;
|
|
||||||
|
|
||||||
[Header("--- 하이라이트 설정 ---")]
|
[Header("--- 참조 ---")] // 인스펙터에 제목을 표시할거에요 -> 참조 섹션을
|
||||||
[ColorUsage(true, true)]
|
[SerializeField] private MeshRenderer weaponRenderer; // 변수를 선언할거에요 -> 하이라이트 줄 렌더러를 weaponRenderer에
|
||||||
[SerializeField] private Color highlightColor = Color.yellow;
|
|
||||||
[SerializeField] private float blinkSpeed = 2f;
|
|
||||||
[SerializeField] private float minIntensity = 0.5f;
|
|
||||||
[SerializeField] private float maxIntensity = 3.0f;
|
|
||||||
|
|
||||||
private Material _material;
|
[Header("--- 하이라이트 설정 ---")] // 인스펙터에 제목을 표시할거에요 -> 하이라이트 설정 섹션을
|
||||||
private static readonly int EmissionProperty = Shader.PropertyToID("_EmissionColor");
|
[ColorUsage(true, true)] // 속성을 붙일거에요 -> HDR 색상도 고를 수 있게
|
||||||
|
[SerializeField] private Color highlightColor = Color.yellow; // 변수를 선언할거에요 -> 발광 색상을 노란색으로
|
||||||
|
[SerializeField] private float blinkSpeed = 2f; // 변수를 선언할거에요 -> 깜빡이는 속도를 2로
|
||||||
|
[SerializeField] private float minIntensity = 0.5f; // 변수를 선언할거에요 -> 최소 발광 세기를 0.5로
|
||||||
|
[SerializeField] private float maxIntensity = 3.0f; // 변수를 선언할거에요 -> 최대 발광 세기를 3.0으로
|
||||||
|
|
||||||
private void Awake()
|
private Material _material; // 변수를 선언할거에요 -> 렌더러의 머티리얼을 담을 _material을
|
||||||
{
|
private static readonly int EmissionProperty = Shader.PropertyToID("_EmissionColor"); // 값을 캐싱할거에요 -> _EmissionColor 프로퍼티 ID를
|
||||||
if (weaponRenderer == null) weaponRenderer = GetComponentInChildren<MeshRenderer>();
|
|
||||||
|
|
||||||
if (weaponRenderer != null)
|
private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> Awake 범위를
|
||||||
_material = weaponRenderer.material;
|
if (weaponRenderer == null) weaponRenderer = GetComponentInChildren<MeshRenderer>(); // 조건이 맞으면 실행할거에요 -> 렌더러가 비어있으면 자식에서 자동으로 찾기
|
||||||
_material.EnableKeyword("_EMISSION");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ⭐ [추가] 부모 오브젝트(손)가 바뀌었을 때 실행됩니다.
|
if (weaponRenderer != null) // 조건을 검사할거에요 -> 렌더러를 찾았는지
|
||||||
private void OnTransformParentChanged()
|
{ // 코드 블록을 시작할거에요 -> 렌더러가 있을 때 처리
|
||||||
{
|
_material = weaponRenderer.material; // 머티리얼을 가져올거에요 -> 렌더러의 material을 _material에
|
||||||
// 부모가 없다면 = 바닥에 떨어졌다면
|
_material.EnableKeyword("_EMISSION"); // 키워드를 켤거에요 -> 발광(Emission) 기능을 활성화
|
||||||
if (transform.parent == null)
|
} // 코드 블록을 끝낼거에요 -> 렌더러 처리
|
||||||
{
|
} // 코드 블록을 끝낼거에요 -> Awake를
|
||||||
this.enabled = true; // 스크립트를 다시 깨웁니다.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Update()
|
// ⭐ [추가] 부모 오브젝트(손)가 바뀌었을 때 실행됩니다. // 설명을 적을거에요 -> 부모 변경 이벤트 훅임을
|
||||||
{
|
private void OnTransformParentChanged() // 함수를 선언할거에요 -> 부모가 바뀌면 자동 호출되는 OnTransformParentChanged를
|
||||||
if (_material == null) return;
|
{ // 코드 블록을 시작할거에요 -> OnTransformParentChanged 범위를
|
||||||
|
// 부모가 없다면 = 바닥에 떨어졌다면 // 설명을 적을거에요 -> 다시 주워지지 않은 상태라는 뜻
|
||||||
|
if (transform.parent == null) // 조건을 검사할거에요 -> 부모가 없는지
|
||||||
|
{ // 코드 블록을 시작할거에요 -> 바닥에 떨어진 경우 처리
|
||||||
|
this.enabled = true; // 스크립트를 켤거에요 -> Update가 다시 돌게
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 바닥 처리
|
||||||
|
} // 코드 블록을 끝낼거에요 -> OnTransformParentChanged를
|
||||||
|
|
||||||
// 누군가 주운 상태(부모가 있음)라면 빛을 끄고 퇴근합니다.
|
private void Update() // 함수를 선언할거에요 -> 매 프레임 실행되는 Update를
|
||||||
if (transform.parent != null)
|
{ // 코드 블록을 시작할거에요 -> Update 범위를
|
||||||
{
|
if (_material == null) return; // 조건이 맞으면 종료할거에요 -> 머티리얼이 없으면 처리 불가
|
||||||
StopHighlight();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
float lerpValue = Mathf.PingPong(Time.time * blinkSpeed, 1f);
|
// 누군가 주운 상태(부모가 있음)라면 빛을 끄고 퇴근합니다. // 설명을 적을거에요 -> 손에 들린 상태면 하이라이트 중단
|
||||||
float intensity = Mathf.Lerp(minIntensity, maxIntensity, lerpValue);
|
if (transform.parent != null) // 조건을 검사할거에요 -> 부모가 있는지(주워졌는지)
|
||||||
_material.SetColor(EmissionProperty, highlightColor * intensity);
|
{ // 코드 블록을 시작할거에요 -> 주워진 경우 처리
|
||||||
}
|
StopHighlight(); // 함수를 실행할거에요 -> 하이라이트 끄기
|
||||||
|
return; // 함수를 끝낼거에요 -> 아래 발광 계산은 안 하게
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 주워진 처리
|
||||||
|
|
||||||
public void StopHighlight()
|
float lerpValue = Mathf.PingPong(Time.time * blinkSpeed, 1f); // 값을 계산할거에요 -> 0~1로 왕복하는 값(깜빡임용)을
|
||||||
{
|
float intensity = Mathf.Lerp(minIntensity, maxIntensity, lerpValue); // 값을 계산할거에요 -> 최소~최대 사이 발광 세기를
|
||||||
if (_material != null)
|
_material.SetColor(EmissionProperty, highlightColor * intensity); // 값을 설정할거에요 -> 발광 색상을 (색 * 세기)로
|
||||||
{
|
} // 코드 블록을 끝낼거에요 -> Update를
|
||||||
_material.SetColor(EmissionProperty, Color.black);
|
|
||||||
this.enabled = false; // Update를 멈춥니다.
|
public void StopHighlight() // 함수를 선언할거에요 -> 하이라이트를 끄는 StopHighlight를
|
||||||
}
|
{ // 코드 블록을 시작할거에요 -> StopHighlight 범위를
|
||||||
}
|
if (_material != null) // 조건을 검사할거에요 -> 머티리얼이 있는지
|
||||||
}
|
{ // 코드 블록을 시작할거에요 -> 머티리얼이 있을 때 처리
|
||||||
|
_material.SetColor(EmissionProperty, Color.black); // 값을 설정할거에요 -> 발광을 검정(=0)으로 만들어 끄기
|
||||||
|
this.enabled = false; // 컴포넌트를 끌거에요 -> Update를 멈추기
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 머티리얼 처리
|
||||||
|
} // 코드 블록을 끝낼거에요 -> StopHighlight를
|
||||||
|
|
||||||
|
} // 코드 블록을 끝낼거에요 -> ItemHighlight를
|
||||||
|
|
@ -1,123 +1,125 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
|
||||||
|
|
||||||
public class BossPatternPhases : MonoBehaviour
|
public class BossPatternPhases : MonoBehaviour // 클래스를 선언할거에요 -> 보스 패턴 페이즈 진행을 관리하는 BossPatternPhases를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> BossPatternPhases 범위를
|
||||||
// [Header]는 유니티 Inspector 창에서 폴더처럼 보이게 해줌
|
|
||||||
[Header("🔧 패턴 설정")]
|
|
||||||
[SerializeField] private int totalPhases = 3; // 전체 패턴 구간 수 (Inspector에서 조절 가능)
|
|
||||||
|
|
||||||
[Header("💎 XP 설정")]
|
// [Header]는 유니티 Inspector 창에서 폴더처럼 보이게 해줌 // 설명을 적을거에요 -> 인스펙터 그룹 라벨용
|
||||||
[SerializeField] private int phaseClearXP = 10; // 한 구간 통과 시 주는 XP
|
[Header("🔧 패턴 설정")] // 인스펙터에 제목을 표시할거에요 -> 패턴 설정을
|
||||||
[SerializeField] private int fullClearBonus = 30; // 모든 구간 통과 시 보너스 XP
|
[SerializeField] private int totalPhases = 3; // 변수를 선언할거에요 -> 전체 페이즈 수를 3으로
|
||||||
[SerializeField] private int hitPenalty = -5; // 피격 시 감점 XP (0으로 하면 감점 없음)
|
|
||||||
|
|
||||||
// [SerializeField]는 인스펙터에서 실시간으로 볼 수 있게 해줌
|
[Header("💎 XP 설정")] // 인스펙터에 제목을 표시할거에요 -> XP 설정을
|
||||||
[Header("🚩 상태 확인용")]
|
[SerializeField] private int phaseClearXP = 10; // 변수를 선언할거에요 -> 페이즈 성공 시 XP를 10으로
|
||||||
[SerializeField] private int currentPhase = 0; // 현재 진행 중인 구간 (0: 시작 전)
|
[SerializeField] private int fullClearBonus = 30; // 변수를 선언할거에요 -> 전체 클리어 보너스를 30으로
|
||||||
[SerializeField] private bool isPatternActive = false; // 패턴이 진행 중인지
|
[SerializeField] private int hitPenalty = -5; // 변수를 선언할거에요 -> 피격 시 감점 XP를 -5로
|
||||||
[SerializeField] private bool wasHitThisPhase = false; // 이번 구간 중 피격당했는지
|
|
||||||
|
|
||||||
// ★ 초기화
|
// [SerializeField]는 인스펙터에서 실시간으로 볼 수 있게 해줌 // 설명을 적을거에요 -> 런타임에도 값 확인 가능
|
||||||
private void Start()
|
[Header("🚩 상태 확인용")] // 인스펙터에 제목을 표시할거에요 -> 상태 확인용을
|
||||||
{
|
[SerializeField] private int currentPhase = 0; // 변수를 선언할거에요 -> 현재 페이즈를 0으로(시작 전)
|
||||||
ResetPattern();
|
[SerializeField] private bool isPatternActive = false; // 변수를 선언할거에요 -> 패턴 진행 여부를 false로
|
||||||
}
|
[SerializeField] private bool wasHitThisPhase = false; // 변수를 선언할거에요 -> 이번 페이즈 피격 여부를 false로
|
||||||
|
|
||||||
// ★ 패턴 전체 리셋 (새로운 패턴 시작 시 호출)
|
// ★ 초기화 // 설명을 적을거에요 -> 시작 시 초기화 수행
|
||||||
public void ResetPattern()
|
private void Start() // 함수를 선언할거에요 -> 시작 시 1회 실행되는 Start를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> Start 범위를
|
||||||
currentPhase = 0;
|
ResetPattern(); // 함수를 실행할거에요 -> 패턴 상태를 초기화
|
||||||
wasHitThisPhase = false;
|
} // 코드 블록을 끝낼거에요 -> Start를
|
||||||
isPatternActive = false;
|
|
||||||
Debug.Log("[보스] 패턴 초기화 완료!");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ★ 패턴 시작 (보스 AI에서 호출)
|
// ★ 패턴 전체 리셋 (새로운 패턴 시작 시 호출) // 설명을 적을거에요 -> 상태를 처음으로 되돌리는 함수
|
||||||
public void StartPattern()
|
public void ResetPattern() // 함수를 선언할거에요 -> 패턴 전체 초기화 ResetPattern을
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> ResetPattern 범위를
|
||||||
if (currentPhase >= totalPhases)
|
currentPhase = 0; // 값을 설정할거에요 -> 현재 페이즈를 0으로
|
||||||
{
|
wasHitThisPhase = false; // 값을 설정할거에요 -> 피격 여부를 false로
|
||||||
Debug.Log("이미 모든 패턴을 완료했어요!");
|
isPatternActive = false; // 값을 설정할거에요 -> 패턴 진행 상태를 false로
|
||||||
return;
|
Debug.Log("[보스] 패턴 초기화 완료!"); // 로그를 찍을거에요 -> 초기화 완료 확인
|
||||||
}
|
} // 코드 블록을 끝낼거에요 -> ResetPattern을
|
||||||
|
|
||||||
isPatternActive = true;
|
// ★ 패턴 시작 (보스 AI에서 호출) // 설명을 적을거에요 -> 보스가 패턴을 시작할 때 호출
|
||||||
wasHitThisPhase = false;
|
public void StartPattern() // 함수를 선언할거에요 -> 패턴 시작 함수 StartPattern을
|
||||||
Debug.Log($"🔔 [페이즈 {currentPhase + 1}] 시작! 회피하세요!");
|
{ // 코드 블록을 시작할거에요 -> StartPattern 범위를
|
||||||
|
if (currentPhase >= totalPhases) // 조건을 검사할거에요 -> 이미 전부 끝났는지
|
||||||
|
{ // 코드 블록을 시작할거에요 -> 전부 끝난 경우 처리
|
||||||
|
Debug.Log("이미 모든 패턴을 완료했어요!"); // 로그를 찍을거에요 -> 더 이상 시작 불가 안내
|
||||||
|
return; // 함수를 끝낼거에요 -> 아래 로직 실행 안 하게
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 완료 처리
|
||||||
|
|
||||||
// ★ ★ ★ 여기서 보스의 애니메이션/이펙트 시작하면 됨 ★ ★ ★
|
isPatternActive = true; // 값을 설정할거에요 -> 패턴 진행 상태를 true로
|
||||||
}
|
wasHitThisPhase = false; // 값을 설정할거에요 -> 이번 페이즈 피격 여부 초기화
|
||||||
|
Debug.Log($"🔔 [페이즈 {currentPhase + 1}] 시작! 회피하세요!"); // 로그를 찍을거에요 -> 페이즈 시작 알림
|
||||||
|
|
||||||
// ★ 플레이어가 피격당했을 때 호출 (PlayerHealth에서 연결)
|
// ★ ★ ★ 여기서 보스의 애니메이션/이펙트 시작하면 됨 ★ ★ ★ // 설명을 적을거에요 -> 연출 시작 지점
|
||||||
public void OnPlayerHit()
|
} // 코드 블록을 끝낼거에요 -> StartPattern을
|
||||||
{
|
|
||||||
if (!isPatternActive) return;
|
|
||||||
|
|
||||||
wasHitThisPhase = true;
|
// ★ 플레이어가 피격당했을 때 호출 (PlayerHealth에서 연결) // 설명을 적을거에요 -> 플레이어 피격 이벤트 연결용
|
||||||
Debug.Log($"💥 [페이즈 {currentPhase + 1}] 중 피격당함! (XP 감점)");
|
public void OnPlayerHit() // 함수를 선언할거에요 -> 피격 처리 함수 OnPlayerHit을
|
||||||
|
{ // 코드 블록을 시작할거에요 -> OnPlayerHit 범위를
|
||||||
|
if (!isPatternActive) return; // 조건이 맞으면 종료할거에요 -> 패턴 중이 아니면 무시
|
||||||
|
|
||||||
// 피격 시 감점 처리 (필요하면)
|
wasHitThisPhase = true; // 값을 설정할거에요 -> 이번 페이즈에서 맞았다고 표시
|
||||||
if (hitPenalty != 0 && ObsessionSystem.instance != null)
|
Debug.Log($"💥 [페이즈 {currentPhase + 1}] 중 피격당함! (XP 감점)"); // 로그를 찍을거에요 -> 피격 안내
|
||||||
{
|
|
||||||
ObsessionSystem.instance.AddRunXP(hitPenalty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ★ 현재 구간을 성공적으로 통과했을 때 호출 (보스 AI에서 호출)
|
// 피격 시 감점 처리 (필요하면) // 설명을 적을거에요 -> hitPenalty가 0이 아니면 XP 감소
|
||||||
public void ClearCurrentPhase()
|
if (hitPenalty != 0 && ObsessionSystem.instance != null) // 조건을 검사할거에요 -> 감점이 있고 시스템이 존재하는지
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> 감점 처리
|
||||||
if (!isPatternActive) return;
|
ObsessionSystem.instance.AddRunXP(hitPenalty); // XP를 더할거에요 -> 음수면 감점됨
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 감점 처리
|
||||||
|
} // 코드 블록을 끝낼거에요 -> OnPlayerHit을
|
||||||
|
|
||||||
// 피격당했는지 확인
|
// ★ 현재 구간을 성공적으로 통과했을 때 호출 (보스 AI에서 호출) // 설명을 적을거에요 -> 페이즈 종료/판정 처리
|
||||||
if (wasHitThisPhase)
|
public void ClearCurrentPhase() // 함수를 선언할거에요 -> 현재 페이즈 클리어 처리 ClearCurrentPhase를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> ClearCurrentPhase 범위를
|
||||||
Debug.Log($"❌ [페이즈 {currentPhase + 1}] 실패... 다음 구간 진행");
|
if (!isPatternActive) return; // 조건이 맞으면 종료할거에요 -> 패턴 중이 아니면 무시
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// ★ 성공 시 XP 지급
|
|
||||||
if (ObsessionSystem.instance != null)
|
|
||||||
{
|
|
||||||
ObsessionSystem.instance.AddRunXP(phaseClearXP);
|
|
||||||
Debug.Log($"✅ [페이즈 {currentPhase + 1}] 성공! +{phaseClearXP}XP");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 다음 구간으로
|
// 피격당했는지 확인 // 설명을 적을거에요 -> 성공/실패 판정 기준
|
||||||
currentPhase++;
|
if (wasHitThisPhase) // 조건을 검사할거에요 -> 이번 페이즈 중 피격 여부
|
||||||
wasHitThisPhase = false;
|
{ // 코드 블록을 시작할거에요 -> 실패 처리
|
||||||
|
Debug.Log($"❌ [페이즈 {currentPhase + 1}] 실패... 다음 구간 진행"); // 로그를 찍을거에요 -> 실패 안내
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 실패 처리
|
||||||
|
else // 조건이 아니면 실행할거에요 -> 안 맞았으면
|
||||||
|
{ // 코드 블록을 시작할거에요 -> 성공 처리
|
||||||
|
// ★ 성공 시 XP 지급 // 설명을 적을거에요 -> 시스템이 있으면 XP 지급
|
||||||
|
if (ObsessionSystem.instance != null) // 조건을 검사할거에요 -> XP 시스템이 존재하는지
|
||||||
|
{ // 코드 블록을 시작할거에요 -> XP 지급
|
||||||
|
ObsessionSystem.instance.AddRunXP(phaseClearXP); // XP를 더할거에요 -> phaseClearXP만큼
|
||||||
|
Debug.Log($"✅ [페이즈 {currentPhase + 1}] 성공! +{phaseClearXP}XP"); // 로그를 찍을거에요 -> 성공 안내
|
||||||
|
} // 코드 블록을 끝낼거에요 -> XP 지급
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 성공/실패 분기
|
||||||
|
|
||||||
// 모든 구간을 완료했는가?
|
// 다음 구간으로 // 설명을 적을거에요 -> 페이즈 진행 업데이트
|
||||||
if (currentPhase >= totalPhases)
|
currentPhase++; // 값을 증가시킬거에요 -> 다음 페이즈로 이동
|
||||||
{
|
wasHitThisPhase = false; // 값을 초기화할거에요 -> 다음 페이즈를 위해 피격 여부 리셋
|
||||||
Debug.Log("🎉 모든 패턴 완료!");
|
|
||||||
FullClearBonus();
|
|
||||||
isPatternActive = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ★ 모든 구간 통과 시 보너스
|
// 모든 구간을 완료했는가? // 설명을 적을거에요 -> 전체 클리어 체크
|
||||||
private void FullClearBonus()
|
if (currentPhase >= totalPhases) // 조건을 검사할거에요 -> 마지막 페이즈까지 끝났는지
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> 전체 클리어 처리
|
||||||
if (ObsessionSystem.instance != null)
|
Debug.Log("🎉 모든 패턴 완료!"); // 로그를 찍을거에요 -> 전체 완료 안내
|
||||||
{
|
FullClearBonus(); // 함수를 실행할거에요 -> 보너스 XP 지급
|
||||||
ObsessionSystem.instance.AddRunXP(fullClearBonus);
|
isPatternActive = false; // 값을 설정할거에요 -> 패턴 종료 상태로
|
||||||
Debug.Log($"🌟 전체 패턴 클리어 보너스 +{fullClearBonus}XP!");
|
} // 코드 블록을 끝낼거에요 -> 전체 클리어 처리
|
||||||
}
|
} // 코드 블록을 끝낼거에요 -> ClearCurrentPhase를
|
||||||
}
|
|
||||||
|
|
||||||
// ★ 디버그용: 현재 상태를 콘솔에 출력
|
// ★ 모든 구간 통과 시 보너스 // 설명을 적을거에요 -> 전체 클리어 보너스 지급
|
||||||
private void Update()
|
private void FullClearBonus() // 함수를 선언할거에요 -> 전체 클리어 보너스 FullClearBonus를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> FullClearBonus 범위를
|
||||||
if (Input.GetKeyDown(KeyCode.P))
|
if (ObsessionSystem.instance != null) // 조건을 검사할거에요 -> XP 시스템이 존재하는지
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> 보너스 지급
|
||||||
Debug.Log($"[패턴 상태] 현재 구간: {currentPhase + 1}/{totalPhases}, 활성: {isPatternActive}");
|
ObsessionSystem.instance.AddRunXP(fullClearBonus); // XP를 더할거에요 -> 보너스 XP 지급
|
||||||
}
|
Debug.Log($"🌟 전체 패턴 클리어 보너스 +{fullClearBonus}XP!"); // 로그를 찍을거에요 -> 보너스 안내
|
||||||
}
|
} // 코드 블록을 끝낼거에요 -> 보너스 지급
|
||||||
|
} // 코드 블록을 끝낼거에요 -> FullClearBonus를
|
||||||
|
|
||||||
// BossPatternPhases.cs에 추가
|
// ★ 디버그용: 현재 상태를 콘솔에 출력 // 설명을 적을거에요 -> 테스트용 단축키 출력
|
||||||
public bool IsPatternActive()
|
private void Update() // 함수를 선언할거에요 -> 매 프레임 호출되는 Update를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> Update 범위를
|
||||||
return isPatternActive;
|
if (Input.GetKeyDown(KeyCode.P)) // 조건을 검사할거에요 -> P 키를 눌렀는지
|
||||||
}
|
{ // 코드 블록을 시작할거에요 -> 디버그 출력
|
||||||
}
|
Debug.Log($"[패턴 상태] 현재 구간: {currentPhase + 1}/{totalPhases}, 활성: {isPatternActive}"); // 로그를 찍을거에요 -> 현재 상태를
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 디버그 출력
|
||||||
|
} // 코드 블록을 끝낼거에요 -> Update를
|
||||||
|
|
||||||
|
// BossPatternPhases.cs에 추가 // 설명을 적을거에요 -> 외부에서 패턴 진행 여부 확인용
|
||||||
|
public bool IsPatternActive() // 함수를 선언할거에요 -> 패턴 활성 여부 반환 함수 IsPatternActive를
|
||||||
|
{ // 코드 블록을 시작할거에요 -> IsPatternActive 범위를
|
||||||
|
return isPatternActive; // 값을 돌려줄거에요 -> 현재 활성 상태를
|
||||||
|
} // 코드 블록을 끝낼거에요 -> IsPatternActive를
|
||||||
|
|
||||||
|
} // 코드 블록을 끝낼거에요 -> BossPatternPhases를
|
||||||
|
|
@ -1,19 +1,22 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
|
||||||
|
|
||||||
public class BossRoomTrigger : MonoBehaviour
|
public class BossRoomTrigger : MonoBehaviour // 클래스를 선언할거에요 -> 보스방 진입 트리거를 처리하는 BossRoomTrigger를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> BossRoomTrigger 범위를
|
||||||
private void OnTriggerEnter(Collider other)
|
|
||||||
{
|
|
||||||
if (other.CompareTag("Player"))
|
|
||||||
{
|
|
||||||
// ★ 보스 방에 처음 들어가면 보너스 XP
|
|
||||||
if (ObsessionSystem.instance != null)
|
|
||||||
{
|
|
||||||
ObsessionSystem.instance.AddRunXP(50); // 보스 도달 보너스
|
|
||||||
Debug.Log("보스 방 도달! +50XP 보너스 획득!");
|
|
||||||
}
|
|
||||||
|
|
||||||
Destroy(this); // 한 번만 주고 제거
|
private void OnTriggerEnter(Collider other) // 함수를 선언할거에요 -> 트리거에 뭔가 들어오면 호출되는 OnTriggerEnter를
|
||||||
}
|
{ // 코드 블록을 시작할거에요 -> OnTriggerEnter 범위를
|
||||||
}
|
if (other.CompareTag("Player")) // 조건을 검사할거에요 -> 들어온 대상이 Player인지
|
||||||
}
|
{ // 코드 블록을 시작할거에요 -> 플레이어일 때 처리
|
||||||
|
|
||||||
|
// ★ 보스 방에 처음 들어가면 보너스 XP // 설명을 적을거에요 -> 1회만 보너스 지급 목적
|
||||||
|
if (ObsessionSystem.instance != null) // 조건을 검사할거에요 -> XP 시스템 싱글톤이 존재하는지
|
||||||
|
{ // 코드 블록을 시작할거에요 -> XP 지급 처리
|
||||||
|
ObsessionSystem.instance.AddRunXP(50); // XP를 더할거에요 -> 보스 도달 보너스 50을
|
||||||
|
Debug.Log("보스 방 도달! +50XP 보너스 획득!"); // 로그를 찍을거에요 -> 보너스 지급 확인
|
||||||
|
} // 코드 블록을 끝낼거에요 -> XP 지급 처리
|
||||||
|
|
||||||
|
Destroy(this); // 컴포넌트를 삭제할거에요 -> 한 번만 발동하게 BossRoomTrigger를
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 플레이어 처리
|
||||||
|
} // 코드 블록을 끝낼거에요 -> OnTriggerEnter를
|
||||||
|
|
||||||
|
} // 코드 블록을 끝낼거에요 -> BossRoomTrigger를
|
||||||
|
|
@ -1,118 +1,110 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
|
||||||
|
|
||||||
public class ObsessionSystem : MonoBehaviour
|
public class ObsessionSystem : MonoBehaviour // 클래스를 선언할거에요 -> 영구 XP/런 XP/레벨을 관리하는 ObsessionSystem을
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> ObsessionSystem 범위를
|
||||||
public static ObsessionSystem instance;
|
|
||||||
|
|
||||||
[SerializeField] private int currentXP = 0; // ★ 영구 XP (저장되는 진짜 진행도)
|
public static ObsessionSystem instance; // 정적 변수를 선언할거에요 -> 어디서든 접근 가능한 싱글톤 instance를
|
||||||
[SerializeField] private int currentLevel = 1;
|
|
||||||
[SerializeField] private int[] xpRequirements = { 100, 300, 500, 800, 1200 };
|
|
||||||
|
|
||||||
// ★ ★ ★ NEW: 이번 런에서 쌓은 임시 XP ★ ★ ★
|
[SerializeField] private int currentXP = 0; // 변수를 선언할거에요 -> 영구 XP(저장되는 진행도)를 currentXP에
|
||||||
[SerializeField] private int currentRunXP = 0; // 이 값은 게임 껐다 키면 사라져요!
|
[SerializeField] private int currentLevel = 1; // 변수를 선언할거에요 -> 현재 레벨을 currentLevel에
|
||||||
|
[SerializeField] private int[] xpRequirements = { 100, 300, 500, 800, 1200 }; // 배열을 선언할거에요 -> 레벨업 필요 XP 목록을
|
||||||
|
|
||||||
private void Awake()
|
[SerializeField] private int currentRunXP = 0; // 변수를 선언할거에요 -> 이번 런에서 쌓인 임시 XP를 currentRunXP에
|
||||||
{
|
|
||||||
if (instance == null)
|
|
||||||
{
|
|
||||||
instance = this;
|
|
||||||
DontDestroyOnLoad(gameObject);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Destroy(gameObject);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Start()
|
private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> Awake 범위를
|
||||||
LoadData();
|
if (instance == null) // 조건을 검사할거에요 -> 싱글톤이 아직 없으면
|
||||||
Debug.Log($"[집착 시스템] 시작! 현재 레벨: {currentLevel}, XP: {currentXP}");
|
{ // 코드 블록을 시작할거에요 -> 최초 생성 처리
|
||||||
|
instance = this; // 값을 넣을거에요 -> instance에 자신을 등록
|
||||||
|
DontDestroyOnLoad(gameObject); // 파괴하지 않을거에요 -> 씬이 바뀌어도 유지되게
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 최초 생성 처리
|
||||||
|
else // 조건이 아니면 실행할거에요 -> 이미 instance가 있으면
|
||||||
|
{ // 코드 블록을 시작할거에요 -> 중복 제거 처리
|
||||||
|
Destroy(gameObject); // 파괴할거에요 -> 중복 오브젝트를
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 중복 제거 처리
|
||||||
|
} // 코드 블록을 끝낼거에요 -> Awake를
|
||||||
|
|
||||||
// ★ ★ ★ NEW: 새 런 시작할 때 임시 XP는 0으로 초기화 ★ ★ ★
|
private void Start() // 함수를 선언할거에요 -> 시작 시 1회 실행되는 Start를
|
||||||
currentRunXP = 0;
|
{ // 코드 블록을 시작할거에요 -> Start 범위를
|
||||||
Debug.Log($"[새 런 시작] 이번 런에 쌓을 임시 XP: 0");
|
LoadData(); // 함수를 실행할거에요 -> 저장된 데이터 불러오기
|
||||||
}
|
Debug.Log($"[집착 시스템] 시작! 현재 레벨: {currentLevel}, XP: {currentXP}"); // 로그를 찍을거에요 -> 시작 상태 출력
|
||||||
|
|
||||||
// ★ ★ ★ NEW: 플레이 중 실시간으로 임시 XP 추가 ★ ★ ★
|
currentRunXP = 0; // 값을 초기화할거에요 -> 새 런 시작이니 임시 XP를 0으로
|
||||||
public void AddRunXP(int amount)
|
Debug.Log($"[새 런 시작] 이번 런에 쌓을 임시 XP: 0"); // 로그를 찍을거에요 -> 런 XP 초기화 확인
|
||||||
{
|
} // 코드 블록을 끝낼거에요 -> Start를
|
||||||
currentRunXP += amount;
|
|
||||||
Debug.Log($"[런 XP 획득] +{amount}XP (이번 런 총: {currentRunXP})");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ★ ★ ★ NEW: 사망 시 임시 XP를 영구 XP로 변환 ★ ★ ★
|
public void AddRunXP(int amount) // 함수를 선언할거에요 -> 플레이 중 임시 XP를 더하는 AddRunXP를
|
||||||
public void OnDeathConvertXP()
|
{ // 코드 블록을 시작할거에요 -> AddRunXP 범위를
|
||||||
{
|
currentRunXP += amount; // 값을 더할거에요 -> 임시 XP에 amount만큼
|
||||||
Debug.Log($"[사망] 이번 런 성과: {currentRunXP}XP를 영구 XP로 전환!");
|
Debug.Log($"[런 XP 획득] +{amount}XP (이번 런 총: {currentRunXP})"); // 로그를 찍을거에요 -> 현재 런 XP 출력
|
||||||
|
} // 코드 블록을 끝낼거에요 -> AddRunXP를
|
||||||
|
|
||||||
// 임시 XP를 영구 XP에 추가
|
public void OnDeathConvertXP() // 함수를 선언할거에요 -> 사망 시 임시 XP를 영구 XP로 바꾸는 OnDeathConvertXP를
|
||||||
AddXP(currentRunXP);
|
{ // 코드 블록을 시작할거에요 -> OnDeathConvertXP 범위를
|
||||||
|
Debug.Log($"[사망] 이번 런 성과: {currentRunXP}XP를 영구 XP로 전환!"); // 로그를 찍을거에요 -> 전환 안내
|
||||||
|
|
||||||
// 임시 XP는 초기화 (다음 런을 위해)
|
AddXP(currentRunXP); // 함수를 실행할거에요 -> 임시 XP만큼 영구 XP에 추가(레벨업/저장 포함)
|
||||||
currentRunXP = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 기존 AddXP 함수 (이제는 사망할 때만 호출될 거예요)
|
currentRunXP = 0; // 값을 초기화할거에요 -> 다음 런을 위해 임시 XP를 0으로
|
||||||
public void AddXP(int amount)
|
} // 코드 블록을 끝낼거에요 -> OnDeathConvertXP를
|
||||||
{
|
|
||||||
currentXP += amount;
|
|
||||||
Debug.Log($"[영구 XP 획득] +{amount}XP (현재: {currentXP})");
|
|
||||||
CheckLevelUp();
|
|
||||||
SaveData();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CheckLevelUp()
|
public void AddXP(int amount) // 함수를 선언할거에요 -> 영구 XP를 더하는 AddXP를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> AddXP 범위를
|
||||||
if (currentLevel - 1 < xpRequirements.Length)
|
currentXP += amount; // 값을 더할거에요 -> 영구 XP에 amount만큼
|
||||||
{
|
Debug.Log($"[영구 XP 획득] +{amount}XP (현재: {currentXP})"); // 로그를 찍을거에요 -> 영구 XP 현황 출력
|
||||||
int requiredXP = xpRequirements[currentLevel - 1];
|
CheckLevelUp(); // 함수를 실행할거에요 -> 레벨업 조건 확인
|
||||||
if (currentXP >= requiredXP)
|
SaveData(); // 함수를 실행할거에요 -> 변경된 데이터 저장
|
||||||
{
|
} // 코드 블록을 끝낼거에요 -> AddXP를
|
||||||
currentLevel++;
|
|
||||||
Debug.Log($"🎉 [레벨업!] 레벨 {currentLevel - 1} → {currentLevel}");
|
|
||||||
UnlockRewards(currentLevel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UnlockRewards(int level)
|
private void CheckLevelUp() // 함수를 선언할거에요 -> 레벨업 여부를 검사하는 CheckLevelUp을
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> CheckLevelUp 범위를
|
||||||
// 보상 코드는 기존과 동일
|
if (currentLevel - 1 < xpRequirements.Length) // 조건을 검사할거에요 -> 요구치 배열 범위 안인지
|
||||||
}
|
{ // 코드 블록을 시작할거에요 -> 범위 안이면
|
||||||
|
int requiredXP = xpRequirements[currentLevel - 1]; // 값을 가져올거에요 -> 현재 레벨의 필요 XP를 requiredXP에
|
||||||
|
if (currentXP >= requiredXP) // 조건을 검사할거에요 -> 영구 XP가 요구치 이상인지
|
||||||
|
{ // 코드 블록을 시작할거에요 -> 레벨업 처리
|
||||||
|
currentLevel++; // 레벨을 올릴거에요 -> 현재 레벨 +1
|
||||||
|
Debug.Log($"🎉 [레벨업!] 레벨 {currentLevel - 1} → {currentLevel}"); // 로그를 찍을거에요 -> 레벨업 안내
|
||||||
|
UnlockRewards(currentLevel); // 함수를 실행할거에요 -> 레벨 보상 해금 처리
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 레벨업 처리
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 범위 체크
|
||||||
|
} // 코드 블록을 끝낼거에요 -> CheckLevelUp을
|
||||||
|
|
||||||
private void SaveData()
|
private void UnlockRewards(int level) // 함수를 선언할거에요 -> 레벨 보상을 해금하는 UnlockRewards를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> UnlockRewards 범위를
|
||||||
PlayerPrefs.SetInt("ObsessionLevel", currentLevel);
|
// 보상 코드는 기존과 동일 // 설명을 적을거에요 -> 보상 구현은 여기에 추가하면 됨
|
||||||
PlayerPrefs.SetInt("ObsessionXP", currentXP);
|
} // 코드 블록을 끝낼거에요 -> UnlockRewards를
|
||||||
PlayerPrefs.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadData()
|
private void SaveData() // 함수를 선언할거에요 -> PlayerPrefs에 저장하는 SaveData를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> SaveData 범위를
|
||||||
currentLevel = PlayerPrefs.GetInt("ObsessionLevel", 1);
|
PlayerPrefs.SetInt("ObsessionLevel", currentLevel); // 값을 저장할거에요 -> 레벨을 ObsessionLevel 키로
|
||||||
currentXP = PlayerPrefs.GetInt("ObsessionXP", 0);
|
PlayerPrefs.SetInt("ObsessionXP", currentXP); // 값을 저장할거에요 -> 영구 XP를 ObsessionXP 키로
|
||||||
}
|
PlayerPrefs.Save(); // 저장을 확정할거에요 -> PlayerPrefs.Save 호출
|
||||||
|
} // 코드 블록을 끝낼거에요 -> SaveData를
|
||||||
|
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
private void LoadData() // 함수를 선언할거에요 -> PlayerPrefs에서 불러오는 LoadData를
|
||||||
// 🧪 테스트용: 현재 상태를 읽는 함수들
|
{ // 코드 블록을 시작할거에요 -> LoadData 범위를
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
currentLevel = PlayerPrefs.GetInt("ObsessionLevel", 1); // 값을 불러올거에요 -> 저장된 레벨이 없으면 1로
|
||||||
|
currentXP = PlayerPrefs.GetInt("ObsessionXP", 0); // 값을 불러올거에요 -> 저장된 XP가 없으면 0으로
|
||||||
|
} // 코드 블록을 끝낼거에요 -> LoadData를
|
||||||
|
|
||||||
// 현재 런 XP 확인
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 구분선을 적을거에요 -> 테스트/조회 섹션 시작을
|
||||||
public int GetCurrentRunXP()
|
// 🧪 테스트용: 현재 상태를 읽는 함수들 // 설명을 적을거에요 -> 외부 조회용 getter들
|
||||||
{
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 구분선을 적을거에요 -> 테스트/조회 섹션을
|
||||||
return currentRunXP;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 현재 영구 XP 확인
|
public int GetCurrentRunXP() // 함수를 선언할거에요 -> 현재 런 XP를 반환하는 GetCurrentRunXP를
|
||||||
public int GetCurrentXP()
|
{ // 코드 블록을 시작할거에요 -> GetCurrentRunXP 범위를
|
||||||
{
|
return currentRunXP; // 값을 돌려줄거에요 -> 임시 XP를
|
||||||
return currentXP;
|
} // 코드 블록을 끝낼거에요 -> GetCurrentRunXP를
|
||||||
}
|
|
||||||
|
|
||||||
// 현재 레벨 확인
|
public int GetCurrentXP() // 함수를 선언할거에요 -> 현재 영구 XP를 반환하는 GetCurrentXP를
|
||||||
public int GetCurrentLevel()
|
{ // 코드 블록을 시작할거에요 -> GetCurrentXP 범위를
|
||||||
{
|
return currentXP; // 값을 돌려줄거에요 -> 영구 XP를
|
||||||
return currentLevel;
|
} // 코드 블록을 끝낼거에요 -> GetCurrentXP를
|
||||||
}
|
|
||||||
}
|
public int GetCurrentLevel() // 함수를 선언할거에요 -> 현재 레벨을 반환하는 GetCurrentLevel을
|
||||||
|
{ // 코드 블록을 시작할거에요 -> GetCurrentLevel 범위를
|
||||||
|
return currentLevel; // 값을 돌려줄거에요 -> 현재 레벨을
|
||||||
|
} // 코드 블록을 끝낼거에요 -> GetCurrentLevel을
|
||||||
|
|
||||||
|
} // 코드 블록을 끝낼거에요 -> ObsessionSystem을
|
||||||
|
|
@ -1,14 +1,18 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
|
||||||
|
|
||||||
public class RoomClear : MonoBehaviour
|
public class RoomClear : MonoBehaviour // 클래스를 선언할거에요 -> 방 클리어 보상을 주는 RoomClear를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> RoomClear 범위를
|
||||||
public void ClearRoom()
|
|
||||||
{
|
public void ClearRoom() // 함수를 선언할거에요 -> 방을 클리어했을 때 호출할 ClearRoom을
|
||||||
// ★ 방을 무사히 클리어하면
|
{ // 코드 블록을 시작할거에요 -> ClearRoom 범위를
|
||||||
if (ObsessionSystem.instance != null)
|
|
||||||
{
|
// ★ 방을 무사히 클리어하면 // 설명을 적을거에요 -> 클리어 보상 지급 구간
|
||||||
ObsessionSystem.instance.AddRunXP(20);
|
if (ObsessionSystem.instance != null) // 조건을 검사할거에요 -> 집착 시스템이 존재하는지
|
||||||
Debug.Log("방 클리어! +20XP");
|
{ // 코드 블록을 시작할거에요 -> XP 지급 처리
|
||||||
}
|
ObsessionSystem.instance.AddRunXP(20); // 함수를 실행할거에요 -> 이번 런 XP에 20 추가
|
||||||
}
|
Debug.Log("방 클리어! +20XP"); // 로그를 찍을거에요 -> 보상 지급 확인
|
||||||
}
|
} // 코드 블록을 끝낼거에요 -> XP 지급 처리
|
||||||
|
|
||||||
|
} // 코드 블록을 끝낼거에요 -> ClearRoom을
|
||||||
|
|
||||||
|
} // 코드 블록을 끝낼거에요 -> RoomClear를
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
public class PlayerAnimator : MonoBehaviour
|
public class PlayerAnimator : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerAnimator를
|
||||||
{
|
{
|
||||||
[SerializeField] private Animator anim;
|
[SerializeField] private Animator anim; // 변수를 선언할거에요 -> 애니메이터 컴포넌트 anim을
|
||||||
[SerializeField] private PlayerHealth health;
|
[SerializeField] private PlayerHealth health; // 변수를 선언할거에요 -> 플레이어 체력 스크립트 health를
|
||||||
|
|
||||||
// ⭐ 추가: 진짜 공격 로직이 있는 스크립트 연결
|
// ⭐ 추가: 진짜 공격 로직이 있는 스크립트 연결
|
||||||
[SerializeField] private PlayerAttack attack;
|
[SerializeField] private PlayerAttack attack; // 변수를 선언할거에요 -> 플레이어 공격 스크립트 attack을
|
||||||
|
|
||||||
private void Awake()
|
private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를
|
||||||
{
|
{
|
||||||
if (health != null)
|
if (health != null) // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있다면
|
||||||
{
|
{
|
||||||
health.OnHitEvent += () => anim.SetTrigger("Hit");
|
health.OnHitEvent += () => anim.SetTrigger("Hit"); // 이벤트를 구독할거에요 -> 피격 시 Hit 트리거 발동을
|
||||||
health.OnDead += () => anim.SetBool("Die", true);
|
health.OnDead += () => anim.SetBool("Die", true); // 이벤트를 구독할거에요 -> 사망 시 Die 상태 변경을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -21,19 +21,19 @@ public class PlayerAnimator : MonoBehaviour
|
||||||
// ⭐ [애니메이션 이벤트 전달자]
|
// ⭐ [애니메이션 이벤트 전달자]
|
||||||
// 애니메이션 창에 적은 이름과 똑같은 함수를 만듭니다.
|
// 애니메이션 창에 적은 이름과 똑같은 함수를 만듭니다.
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
public void StartWeaponCollision()
|
public void StartWeaponCollision() // 함수를 선언할거에요 -> 무기 충돌 시작 이벤트 StartWeaponCollision을
|
||||||
{
|
{
|
||||||
if (attack != null) attack.StartWeaponCollision();
|
if (attack != null) attack.StartWeaponCollision(); // 실행할거에요 -> 공격 스크립트의 충돌 시작 함수를
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StopWeaponCollision()
|
public void StopWeaponCollision() // 함수를 선언할거에요 -> 무기 충돌 종료 이벤트 StopWeaponCollision을
|
||||||
{
|
{
|
||||||
if (attack != null) attack.StopWeaponCollision();
|
if (attack != null) attack.StopWeaponCollision(); // 실행할거에요 -> 공격 스크립트의 충돌 종료 함수를
|
||||||
}
|
}
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
public void UpdateMove(float speedValue) => anim.SetFloat("Speed", speedValue);
|
public void UpdateMove(float speedValue) => anim.SetFloat("Speed", speedValue); // 값을 전달할거에요 -> 애니메이터의 Speed 파라미터에
|
||||||
// public void TriggerAttack() => anim.SetTrigger("Attack");
|
// public void TriggerAttack() => anim.SetTrigger("Attack");
|
||||||
public void TriggerThrow() => anim.SetTrigger("Throw");
|
public void TriggerThrow() => anim.SetTrigger("Throw"); // 신호를 보낼거에요 -> 애니메이터의 Throw 트리거에
|
||||||
public void SetCharging(bool isCharging) => anim.SetBool("isCharging", isCharging);
|
public void SetCharging(bool isCharging) => anim.SetBool("isCharging", isCharging); // 값을 설정할거에요 -> 애니메이터의 isCharging 불리언에
|
||||||
}
|
}
|
||||||
|
|
@ -1,57 +1,57 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using System.Collections;
|
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 발사된 화살 발사체 (파티클 프리팹에 부착됨)
|
/// 발사된 화살 발사체 (파티클 프리팹에 부착됨)
|
||||||
/// SphereCast 기반 정밀 충돌 감지 (기존 100% 유지)
|
/// SphereCast 기반 정밀 충돌 감지 (기존 100% 유지)
|
||||||
/// [NEW] 속성 데미지(DoT) 시스템 추가
|
/// [NEW] 속성 데미지(DoT) 시스템 추가
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PlayerArrow : MonoBehaviour
|
public class PlayerArrow : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerArrow를
|
||||||
{
|
{
|
||||||
[Header("--- 정밀 피격 판정 설정 ---")]
|
[Header("--- 정밀 피격 판정 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 정밀 피격 판정 설정 --- 을
|
||||||
[SerializeField] private float raycastDistance = 0.8f;
|
[SerializeField] private float raycastDistance = 0.8f; // 변수를 선언할거에요 -> 레이캐스트 거리인 raycastDistance를
|
||||||
[SerializeField] private float raycastRadius = 0.08f;
|
[SerializeField] private float raycastRadius = 0.08f; // 변수를 선언할거에요 -> 레이캐스트 반지름인 raycastRadius를
|
||||||
[SerializeField] private LayerMask hitLayers;
|
[SerializeField] private LayerMask hitLayers; // 변수를 선언할거에요 -> 충돌 레이어인 hitLayers를
|
||||||
[SerializeField] private Transform arrowTip;
|
[SerializeField] private Transform arrowTip; // 변수를 선언할거에요 -> 화살 끝부분 위치인 arrowTip을
|
||||||
|
|
||||||
[Header("--- 디버그 시각화 ---")]
|
[Header("--- 디버그 시각화 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 디버그 시각화 --- 를
|
||||||
[SerializeField] private bool showDebugRay = true;
|
[SerializeField] private bool showDebugRay = true; // 변수를 선언할거에요 -> 디버그 레이 표시 여부인 showDebugRay를
|
||||||
|
|
||||||
// 화살 스탯
|
// 화살 스탯
|
||||||
private float damage;
|
private float damage; // 변수를 선언할거에요 -> 데미지 damage를
|
||||||
private float speed;
|
private float speed; // 변수를 선언할거에요 -> 속도 speed를
|
||||||
private float range;
|
private float range; // 변수를 선언할거에요 -> 사거리 range를
|
||||||
private Vector3 startPos;
|
private Vector3 startPos; // 변수를 선언할거에요 -> 시작 위치 startPos를
|
||||||
private Vector3 shootDirection;
|
private Vector3 shootDirection; // 변수를 선언할거에요 -> 발사 방향 shootDirection을
|
||||||
private bool isFired = false;
|
private bool isFired = false; // 변수를 초기화할거에요 -> 발사 여부 isFired를 거짓으로
|
||||||
private bool hasHit = false;
|
private bool hasHit = false; // 변수를 초기화할거에요 -> 충돌 여부 hasHit을 거짓으로
|
||||||
|
|
||||||
// [NEW] 속성 데미지 시스템
|
// [NEW] 속성 데미지 시스템
|
||||||
private ArrowElementType elementType = ArrowElementType.None;
|
private ArrowElementType elementType = ArrowElementType.None; // 변수를 초기화할거에요 -> 속성 타입 elementType을 없음으로
|
||||||
private float elementDamage = 0f;
|
private float elementDamage = 0f; // 변수를 초기화할거에요 -> 속성 데미지 elementDamage를 0으로
|
||||||
private float elementDuration = 0f;
|
private float elementDuration = 0f; // 변수를 초기화할거에요 -> 속성 지속 시간 elementDuration을 0으로
|
||||||
|
|
||||||
// Raycast 추적용
|
// Raycast 추적용
|
||||||
private Vector3 previousPosition;
|
private Vector3 previousPosition; // 변수를 선언할거에요 -> 이전 위치 previousPosition을
|
||||||
private Rigidbody rb;
|
private Rigidbody rb; // 변수를 선언할거에요 -> 물리 컴포넌트 rb를
|
||||||
|
|
||||||
private void Awake()
|
private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를
|
||||||
{
|
{
|
||||||
rb = GetComponent<Rigidbody>();
|
rb = GetComponent<Rigidbody>(); // 컴포넌트를 가져올거에요 -> 리지드바디를
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Start()
|
private void Start() // 함수를 실행할거에요 -> 활성화 시 Start를
|
||||||
{
|
{
|
||||||
if (arrowTip == null)
|
if (arrowTip == null) // 조건이 맞으면 실행할거에요 -> 화살 팁이 설정되지 않았다면
|
||||||
{
|
{
|
||||||
Transform tip = transform.Find("ArrowTip");
|
Transform tip = transform.Find("ArrowTip"); // 찾을거에요 -> 자식 중 ArrowTip을
|
||||||
if (tip == null) tip = transform.Find("Tip");
|
if (tip == null) tip = transform.Find("Tip"); // 못 찾으면 찾을거에요 -> Tip을
|
||||||
arrowTip = tip ?? transform;
|
arrowTip = tip ?? transform; // 그래도 없으면 할당할거에요 -> 자기 자신의 위치를
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hitLayers.value == 0)
|
if (hitLayers.value == 0) // 조건이 맞으면 실행할거에요 -> 충돌 레이어가 설정되지 않았다면
|
||||||
{
|
{
|
||||||
hitLayers = LayerMask.GetMask("Enemy", "EnemyHitBox", "Wall", "Ground", "Default");
|
hitLayers = LayerMask.GetMask("Enemy", "EnemyHitBox", "Wall", "Ground", "Default"); // 값을 넣을거에요 -> 기본 충돌 레이어들을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,83 +66,83 @@ public class PlayerArrow : MonoBehaviour
|
||||||
Vector3 direction,
|
Vector3 direction,
|
||||||
ArrowElementType element,
|
ArrowElementType element,
|
||||||
float elemDmg,
|
float elemDmg,
|
||||||
float elemDur)
|
float elemDur) // 함수를 선언할거에요 -> 화살을 초기화하는 Initialize를
|
||||||
{
|
{
|
||||||
this.damage = dmg;
|
this.damage = dmg; // 값을 저장할거에요 -> 데미지를
|
||||||
this.speed = arrowSpeed;
|
this.speed = arrowSpeed; // 값을 저장할거에요 -> 속도를
|
||||||
this.range = maxRange;
|
this.range = maxRange; // 값을 저장할거에요 -> 사거리를
|
||||||
this.shootDirection = direction.normalized;
|
this.shootDirection = direction.normalized; // 값을 저장할거에요 -> 정규화된 발사 방향을
|
||||||
this.startPos = transform.position;
|
this.startPos = transform.position; // 값을 저장할거에요 -> 시작 위치를
|
||||||
this.previousPosition = transform.position;
|
this.previousPosition = transform.position; // 값을 저장할거에요 -> 이전 위치를 (레이캐스트용)
|
||||||
this.isFired = true;
|
this.isFired = true; // 상태를 바꿀거에요 -> 발사됨 상태로
|
||||||
|
|
||||||
// [NEW] 속성 정보 저장
|
// [NEW] 속성 정보 저장
|
||||||
this.elementType = element;
|
this.elementType = element; // 값을 저장할거에요 -> 속성 타입을
|
||||||
this.elementDamage = elemDmg;
|
this.elementDamage = elemDmg; // 값을 저장할거에요 -> 속성 데미지를
|
||||||
this.elementDuration = elemDur;
|
this.elementDuration = elemDur; // 값을 저장할거에요 -> 속성 지속 시간을
|
||||||
|
|
||||||
// 발사 방향으로 회전
|
// 발사 방향으로 회전
|
||||||
if (shootDirection != Vector3.zero)
|
if (shootDirection != Vector3.zero) // 조건이 맞으면 실행할거에요 -> 방향이 유효하다면
|
||||||
{
|
{
|
||||||
transform.rotation = Quaternion.LookRotation(shootDirection);
|
transform.rotation = Quaternion.LookRotation(shootDirection); // 회전시킬거에요 -> 발사 방향을 보도록
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rigidbody 설정
|
// Rigidbody 설정
|
||||||
if (rb == null) rb = GetComponent<Rigidbody>();
|
if (rb == null) rb = GetComponent<Rigidbody>(); // 컴포넌트를 가져올거에요 -> 없다면 리지드바디를
|
||||||
|
|
||||||
if (rb != null)
|
if (rb != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
|
||||||
{
|
{
|
||||||
rb.useGravity = false;
|
rb.useGravity = false; // 설정을 바꿀거에요 -> 중력을 끄기로
|
||||||
rb.isKinematic = false;
|
rb.isKinematic = false; // 설정을 바꿀거에요 -> 물리 연산을 켜기로
|
||||||
rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
|
rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; // 설정을 바꿀거에요 -> 연속 충돌 감지로
|
||||||
rb.velocity = shootDirection * speed;
|
rb.velocity = shootDirection * speed; // 값을 넣을거에요 -> 방향과 속도를 곱해 물리 속도로
|
||||||
}
|
}
|
||||||
|
|
||||||
Destroy(gameObject, 5f);
|
Destroy(gameObject, 5f); // 파괴할거에요 -> 5초 뒤에 안전장치로
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// [하위 호환성] 방향 없는 3-파라미터 버전
|
/// [하위 호환성] 방향 없는 3-파라미터 버전
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Initialize(float dmg, float arrowSpeed, float maxRange)
|
public void Initialize(float dmg, float arrowSpeed, float maxRange) // 함수를 선언할거에요 -> 구버전 호환용 초기화 함수를
|
||||||
{
|
{
|
||||||
Initialize(dmg, arrowSpeed, maxRange, transform.forward,
|
Initialize(dmg, arrowSpeed, maxRange, transform.forward,
|
||||||
ArrowElementType.None, 0f, 0f);
|
ArrowElementType.None, 0f, 0f); // 실행할거에요 -> 신버전 초기화 함수를 기본값으로
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를
|
||||||
{
|
{
|
||||||
if (!isFired || hasHit) return;
|
if (!isFired || hasHit) return; // 조건이 맞으면 중단할거에요 -> 발사되지 않았거나 이미 맞았다면
|
||||||
|
|
||||||
// 사거리 체크
|
// 사거리 체크
|
||||||
float traveledDistance = Vector3.Distance(startPos, transform.position);
|
float traveledDistance = Vector3.Distance(startPos, transform.position); // 거리를 계산할거에요 -> 이동 거리를
|
||||||
if (traveledDistance >= range)
|
if (traveledDistance >= range) // 조건이 맞으면 실행할거에요 -> 사거리를 넘었다면
|
||||||
{
|
{
|
||||||
Destroy(gameObject);
|
Destroy(gameObject); // 파괴할거에요 -> 화살을
|
||||||
return;
|
return; // 중단할거에요 -> 함수를
|
||||||
}
|
}
|
||||||
|
|
||||||
// 정밀 충돌 감지
|
// 정밀 충돌 감지
|
||||||
CheckPrecisionCollision();
|
CheckPrecisionCollision(); // 함수를 실행할거에요 -> 정밀 충돌 감지 함수를
|
||||||
|
|
||||||
previousPosition = transform.position;
|
previousPosition = transform.position; // 값을 저장할거에요 -> 현재 위치를 이전 위치로
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// SphereCast 기반 정밀 충돌 (기존 로직 100% 유지)
|
/// SphereCast 기반 정밀 충돌 (기존 로직 100% 유지)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void CheckPrecisionCollision()
|
private void CheckPrecisionCollision() // 함수를 선언할거에요 -> 정밀 충돌을 체크하는 CheckPrecisionCollision을
|
||||||
{
|
{
|
||||||
Vector3 tipPosition = arrowTip != null ? arrowTip.position : transform.position;
|
Vector3 tipPosition = arrowTip != null ? arrowTip.position : transform.position; // 위치를 결정할거에요 -> 팁 위치 혹은 내 위치를
|
||||||
|
|
||||||
Vector3 direction = rb != null && rb.velocity.magnitude > 0.1f
|
Vector3 direction = rb != null && rb.velocity.magnitude > 0.1f
|
||||||
? rb.velocity.normalized
|
? rb.velocity.normalized
|
||||||
: transform.forward;
|
: transform.forward; // 방향을 결정할거에요 -> 물리 속도 방향 혹은 정면을
|
||||||
|
|
||||||
float frameDistance = Vector3.Distance(previousPosition, transform.position);
|
float frameDistance = Vector3.Distance(previousPosition, transform.position); // 거리를 계산할거에요 -> 프레임 간 이동 거리를
|
||||||
float checkDistance = Mathf.Max(frameDistance, raycastDistance);
|
float checkDistance = Mathf.Max(frameDistance, raycastDistance); // 값을 결정할거에요 -> 이동 거리와 최소 거리 중 큰 값을
|
||||||
|
|
||||||
RaycastHit hit;
|
RaycastHit hit; // 변수를 선언할거에요 -> 충돌 정보를 담을 hit을
|
||||||
bool didHit = Physics.SphereCast(
|
bool didHit = Physics.SphereCast(
|
||||||
tipPosition,
|
tipPosition,
|
||||||
raycastRadius,
|
raycastRadius,
|
||||||
|
|
@ -150,64 +150,64 @@ public class PlayerArrow : MonoBehaviour
|
||||||
out hit,
|
out hit,
|
||||||
checkDistance,
|
checkDistance,
|
||||||
hitLayers
|
hitLayers
|
||||||
);
|
); // 실행할거에요 -> 구체 캐스트를 쏴서 충돌 여부를 확인하는 것을
|
||||||
|
|
||||||
if (showDebugRay)
|
if (showDebugRay) // 조건이 맞으면 실행할거에요 -> 디버그 모드라면
|
||||||
{
|
{
|
||||||
Debug.DrawRay(tipPosition, direction * checkDistance, didHit ? Color.red : Color.green, 0.1f);
|
Debug.DrawRay(tipPosition, direction * checkDistance, didHit ? Color.red : Color.green, 0.1f); // 선을 그릴거에요 -> 충돌 시 빨강, 아니면 초록으로
|
||||||
}
|
}
|
||||||
|
|
||||||
if (didHit)
|
if (didHit) // 조건이 맞으면 실행할거에요 -> 충돌했다면
|
||||||
{
|
{
|
||||||
HandleHit(hit.collider, hit.point);
|
HandleHit(hit.collider, hit.point); // 실행할거에요 -> 충돌 처리 함수 HandleHit을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// [MODIFIED] 충돌 처리 — 속성 데미지 적용 추가
|
/// [MODIFIED] 충돌 처리 — 속성 데미지 적용 추가
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void HandleHit(Collider hitCollider, Vector3 hitPoint)
|
private void HandleHit(Collider hitCollider, Vector3 hitPoint) // 함수를 선언할거에요 -> 충돌 결과를 처리하는 HandleHit을
|
||||||
{
|
{
|
||||||
if (hasHit) return;
|
if (hasHit) return; // 조건이 맞으면 중단할거에요 -> 이미 충돌 처리되었다면
|
||||||
hasHit = true;
|
hasHit = true; // 상태를 바꿀거에요 -> 충돌 처리됨으로
|
||||||
|
|
||||||
// 적 감지 (Tag 또는 Layer)
|
// 적 감지 (Tag 또는 Layer)
|
||||||
bool isEnemy = hitCollider.CompareTag("Enemy");
|
bool isEnemy = hitCollider.CompareTag("Enemy"); // 조건을 확인할거에요 -> 적 태그인지
|
||||||
|
|
||||||
// EnemyHitBox 레이어 체크 (레이어가 존재하는 경우에만)
|
// EnemyHitBox 레이어 체크 (레이어가 존재하는 경우에만)
|
||||||
int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox");
|
int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox"); // 값을 가져올거에요 -> 레이어 인덱스를
|
||||||
if (!isEnemy && enemyHitBoxLayerIndex != -1)
|
if (!isEnemy && enemyHitBoxLayerIndex != -1) // 조건이 맞으면 실행할거에요 -> 적 태그가 아니고 레이어가 유효하다면
|
||||||
{
|
{
|
||||||
isEnemy = hitCollider.gameObject.layer == enemyHitBoxLayerIndex;
|
isEnemy = hitCollider.gameObject.layer == enemyHitBoxLayerIndex; // 조건을 갱신할거에요 -> 해당 레이어인지 확인해서
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEnemy)
|
if (isEnemy) // 조건이 맞으면 실행할거에요 -> 적이라면
|
||||||
{
|
{
|
||||||
// MonsterClass를 찾아 데미지 적용
|
// MonsterClass를 찾아 데미지 적용
|
||||||
MonsterClass monster = hitCollider.GetComponentInParent<MonsterClass>();
|
MonsterClass monster = hitCollider.GetComponentInParent<MonsterClass>(); // 컴포넌트를 찾을거에요 -> 부모의 몬스터 클래스를
|
||||||
if (monster == null) monster = hitCollider.GetComponent<MonsterClass>();
|
if (monster == null) monster = hitCollider.GetComponent<MonsterClass>(); // 없으면 찾을거에요 -> 자신의 몬스터 클래스를
|
||||||
|
|
||||||
if (monster != null)
|
if (monster != null) // 조건이 맞으면 실행할거에요 -> 몬스터 스크립트가 있다면
|
||||||
{
|
{
|
||||||
// 1. 기본 데미지 적용
|
// 1. 기본 데미지 적용
|
||||||
monster.TakeDamage(damage);
|
monster.TakeDamage(damage); // 실행할거에요 -> 데미지 입히기 함수를
|
||||||
Debug.Log($"적 명중! 기본데미지: {damage}");
|
Debug.Log($"적 명중! 기본데미지: {damage}"); // 로그를 출력할거에요 -> 명중 메시지를
|
||||||
|
|
||||||
// 2. [NEW] 속성 효과 적용
|
// 2. [NEW] 속성 효과 적용
|
||||||
ApplyElementEffect(monster);
|
ApplyElementEffect(monster); // 실행할거에요 -> 속성 효과 적용 함수를
|
||||||
}
|
}
|
||||||
|
|
||||||
Destroy(gameObject, 0.05f);
|
Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을 잠시 뒤
|
||||||
}
|
}
|
||||||
else if (hitCollider.CompareTag("Wall") || hitCollider.CompareTag("Ground"))
|
else if (hitCollider.CompareTag("Wall") || hitCollider.CompareTag("Ground")) // 조건이 틀리면 실행할거에요 -> 벽이나 땅이라면
|
||||||
{
|
{
|
||||||
Debug.Log($"벽/바닥 충돌! 위치: {hitPoint}");
|
Debug.Log($"벽/바닥 충돌! 위치: {hitPoint}"); // 로그를 출력할거에요 -> 충돌 위치를
|
||||||
Destroy(gameObject, 0.05f);
|
Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을
|
||||||
}
|
}
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 그 외의 충돌이라면
|
||||||
{
|
{
|
||||||
// 기타 충돌 (Unknown)
|
// 기타 충돌 (Unknown)
|
||||||
Destroy(gameObject, 0.05f);
|
Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,31 +215,31 @@ public class PlayerArrow : MonoBehaviour
|
||||||
/// [NEW] 속성 효과 적용
|
/// [NEW] 속성 효과 적용
|
||||||
/// MonsterClass에 ApplyStatusEffect 메서드가 있어야 합니다.
|
/// MonsterClass에 ApplyStatusEffect 메서드가 있어야 합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void ApplyElementEffect(MonsterClass monster)
|
private void ApplyElementEffect(MonsterClass monster) // 함수를 선언할거에요 -> 속성 효과를 적용하는 ApplyElementEffect를
|
||||||
{
|
{
|
||||||
if (elementType == ArrowElementType.None || elementDamage <= 0f) return;
|
if (elementType == ArrowElementType.None || elementDamage <= 0f) return; // 조건이 맞으면 중단할거에요 -> 속성이 없거나 데미지가 없다면
|
||||||
|
|
||||||
// MonsterClass에 ApplyStatusEffect가 있는지 확인
|
// MonsterClass에 ApplyStatusEffect가 있는지 확인
|
||||||
switch (elementType)
|
switch (elementType) // 분기할거에요 -> 속성 타입에 따라
|
||||||
{
|
{
|
||||||
case ArrowElementType.Fire:
|
case ArrowElementType.Fire: // 조건이 맞으면 실행할거에요 -> 불 속성이라면
|
||||||
monster.ApplyStatusEffect(StatusEffectType.Burn, elementDamage, elementDuration);
|
monster.ApplyStatusEffect(StatusEffectType.Burn, elementDamage, elementDuration); // 실행할거에요 -> 화상 효과 적용을
|
||||||
Debug.Log($"화염 효과! {elementDamage} 데미지 x {elementDuration}초");
|
Debug.Log($"화염 효과! {elementDamage} 데미지 x {elementDuration}초"); // 로그를 출력할거에요 -> 효과 설명을
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ArrowElementType.Ice:
|
case ArrowElementType.Ice: // 조건이 맞으면 실행할거에요 -> 얼음 속성이라면
|
||||||
monster.ApplyStatusEffect(StatusEffectType.Slow, elementDamage, elementDuration);
|
monster.ApplyStatusEffect(StatusEffectType.Slow, elementDamage, elementDuration); // 실행할거에요 -> 슬로우 효과 적용을
|
||||||
Debug.Log($"빙결 효과! 슬로우 {elementDuration}초");
|
Debug.Log($"빙결 효과! 슬로우 {elementDuration}초"); // 로그를 출력할거에요 -> 효과 설명을
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ArrowElementType.Poison:
|
case ArrowElementType.Poison: // 조건이 맞으면 실행할거에요 -> 독 속성이라면
|
||||||
monster.ApplyStatusEffect(StatusEffectType.Poison, elementDamage, elementDuration);
|
monster.ApplyStatusEffect(StatusEffectType.Poison, elementDamage, elementDuration); // 실행할거에요 -> 독 효과 적용을
|
||||||
Debug.Log($"독 효과! {elementDamage} 데미지 x {elementDuration}초");
|
Debug.Log($"독 효과! {elementDamage} 데미지 x {elementDuration}초"); // 로그를 출력할거에요 -> 효과 설명을
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ArrowElementType.Lightning:
|
case ArrowElementType.Lightning: // 조건이 맞으면 실행할거에요 -> 번개 속성이라면
|
||||||
monster.ApplyStatusEffect(StatusEffectType.Shock, elementDamage, elementDuration);
|
monster.ApplyStatusEffect(StatusEffectType.Shock, elementDamage, elementDuration); // 실행할거에요 -> 감전(스턴) 효과 적용을
|
||||||
Debug.Log($"감전 효과! {elementDamage} 범위 데미지");
|
Debug.Log($"감전 효과! {elementDamage} 범위 데미지"); // 로그를 출력할거에요 -> 효과 설명을
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -247,58 +247,58 @@ public class PlayerArrow : MonoBehaviour
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 백업 충돌 감지 (OnTriggerEnter) — 기존 로직 유지
|
/// 백업 충돌 감지 (OnTriggerEnter) — 기존 로직 유지
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void OnTriggerEnter(Collider other)
|
private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 트리거 충돌 시 OnTriggerEnter를
|
||||||
{
|
{
|
||||||
if (hasHit) return;
|
if (hasHit) return; // 조건이 맞으면 중단할거에요 -> 이미 맞았다면
|
||||||
|
|
||||||
bool isEnemy = other.CompareTag("Enemy");
|
bool isEnemy = other.CompareTag("Enemy"); // 조건을 확인할거에요 -> 적 태그인지
|
||||||
int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox");
|
int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox"); // 값을 가져올거에요 -> 히트박스 레이어 인덱스를
|
||||||
if (!isEnemy && enemyHitBoxLayerIndex != -1)
|
if (!isEnemy && enemyHitBoxLayerIndex != -1) // 조건이 맞으면 실행할거에요 -> 적 태그가 아니고 레이어가 유효하다면
|
||||||
{
|
{
|
||||||
isEnemy = other.gameObject.layer == enemyHitBoxLayerIndex;
|
isEnemy = other.gameObject.layer == enemyHitBoxLayerIndex; // 조건을 갱신할거에요 -> 레이어 확인으로
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEnemy)
|
if (isEnemy) // 조건이 맞으면 실행할거에요 -> 적이라면
|
||||||
{
|
{
|
||||||
HandleHit(other, other.ClosestPoint(transform.position));
|
HandleHit(other, other.ClosestPoint(transform.position)); // 실행할거에요 -> 충돌 처리 함수를
|
||||||
}
|
}
|
||||||
else if (other.CompareTag("Wall") || other.CompareTag("Ground"))
|
else if (other.CompareTag("Wall") || other.CompareTag("Ground")) // 조건이 틀리면 실행할거에요 -> 벽이나 땅이라면
|
||||||
{
|
{
|
||||||
hasHit = true;
|
hasHit = true; // 상태를 바꿀거에요 -> 충돌 처리됨으로
|
||||||
Destroy(gameObject, 0.05f);
|
Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gizmo 디버그 시각화 — 기존 로직 유지 + 속성 색상 추가
|
/// Gizmo 디버그 시각화 — 기존 로직 유지 + 속성 색상 추가
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void OnDrawGizmos()
|
private void OnDrawGizmos() // 함수를 실행할거에요 -> 기즈모를 그리는 OnDrawGizmos를
|
||||||
{
|
{
|
||||||
if (!Application.isPlaying || !isFired || !showDebugRay) return;
|
if (!Application.isPlaying || !isFired || !showDebugRay) return; // 조건이 맞으면 중단할거에요 -> 실행 중이 아니거나 발사 전이거나 디버그가 꺼졌다면
|
||||||
|
|
||||||
Vector3 tipPosition = arrowTip != null ? arrowTip.position : transform.position;
|
Vector3 tipPosition = arrowTip != null ? arrowTip.position : transform.position; // 위치를 결정할거에요 -> 팁 위치 혹은 내 위치로
|
||||||
Vector3 direction = rb != null && rb.velocity.magnitude > 0.1f
|
Vector3 direction = rb != null && rb.velocity.magnitude > 0.1f
|
||||||
? rb.velocity.normalized
|
? rb.velocity.normalized
|
||||||
: transform.forward;
|
: transform.forward; // 방향을 결정할거에요 -> 물리 속도 방향 혹은 정면으로
|
||||||
|
|
||||||
Gizmos.color = hasHit ? Color.red : Color.green;
|
Gizmos.color = hasHit ? Color.red : Color.green; // 색상을 설정할거에요 -> 맞았으면 빨강, 아니면 초록으로
|
||||||
Gizmos.DrawWireSphere(tipPosition, raycastRadius);
|
Gizmos.DrawWireSphere(tipPosition, raycastRadius); // 그림을 그릴거에요 -> 팁 위치에 구체를
|
||||||
Gizmos.DrawWireSphere(tipPosition + direction * raycastDistance, raycastRadius);
|
Gizmos.DrawWireSphere(tipPosition + direction * raycastDistance, raycastRadius); // 그림을 그릴거에요 -> 예상 도달 위치에 구체를
|
||||||
|
|
||||||
Gizmos.color = Color.yellow;
|
Gizmos.color = Color.yellow; // 색상을 설정할거에요 -> 노란색으로
|
||||||
Gizmos.DrawLine(tipPosition, tipPosition + direction * raycastDistance);
|
Gizmos.DrawLine(tipPosition, tipPosition + direction * raycastDistance); // 선을 그릴거에요 -> 궤적을 표시하는
|
||||||
|
|
||||||
// [NEW] 속성별 색상 표시
|
// [NEW] 속성별 색상 표시
|
||||||
switch (elementType)
|
switch (elementType) // 분기할거에요 -> 속성 타입에 따라
|
||||||
{
|
{
|
||||||
case ArrowElementType.Fire: Gizmos.color = Color.red; break;
|
case ArrowElementType.Fire: Gizmos.color = Color.red; break; // 색상을 바꿀거에요 -> 빨강으로
|
||||||
case ArrowElementType.Ice: Gizmos.color = Color.cyan; break;
|
case ArrowElementType.Ice: Gizmos.color = Color.cyan; break; // 색상을 바꿀거에요 -> 청록으로
|
||||||
case ArrowElementType.Poison: Gizmos.color = Color.green; break;
|
case ArrowElementType.Poison: Gizmos.color = Color.green; break; // 색상을 바꿀거에요 -> 초록으로
|
||||||
case ArrowElementType.Lightning: Gizmos.color = Color.yellow; break;
|
case ArrowElementType.Lightning: Gizmos.color = Color.yellow; break; // 색상을 바꿀거에요 -> 노랑으로
|
||||||
}
|
}
|
||||||
if (elementType != ArrowElementType.None)
|
if (elementType != ArrowElementType.None) // 조건이 맞으면 실행할거에요 -> 속성이 있다면
|
||||||
{
|
{
|
||||||
Gizmos.DrawWireSphere(transform.position, 0.5f);
|
Gizmos.DrawWireSphere(transform.position, 0.5f); // 그림을 그릴거에요 -> 속성 표시용 구체를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,46 +1,18 @@
|
||||||
// ============================================
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
// 📁 파일명: ArrowData.cs
|
|
||||||
// 📂 경로: Assets/Scripts/Data/ArrowData.cs
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
using UnityEngine;
|
[CreateAssetMenu(fileName = "New Arrow Data", menuName = "Combat/Arrow Data")] // 메뉴를 만들거에요 -> 에셋 메뉴를
|
||||||
|
public class ArrowData : ScriptableObject // 클래스를 선언할거에요 -> 데이터 컨테이너인 ArrowData를
|
||||||
/// <summary>
|
|
||||||
/// 화살 정보를 담는 데이터 구조체
|
|
||||||
/// ArrowPickup.GetArrowData() → PlayerAttack.SetCurrentArrow() 로 전달됩니다.
|
|
||||||
/// </summary>
|
|
||||||
[System.Serializable]
|
|
||||||
public struct ArrowData
|
|
||||||
{
|
{
|
||||||
[Header("--- 기본 정보 ---")]
|
[Header("기본 정보")] // 인스펙터 창에 제목을 표시할거에요 -> 기본 정보 를
|
||||||
public string arrowName; // 화살 이름 (UI 표시용)
|
public string arrowName; // 변수를 선언할거에요 -> 화살 이름을
|
||||||
public ArrowElementType elementType; // 속성 타입
|
public GameObject projectilePrefab; // 변수를 선언할거에요 -> 투사체 프리팹을
|
||||||
|
public Sprite icon; // 변수를 선언할거에요 -> 아이콘 이미지를
|
||||||
|
|
||||||
[Header("--- 스탯 ---")]
|
[Header("데미지 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 데미지 설정 을
|
||||||
public float baseDamage; // 기본 데미지
|
public float baseDamage = 10f; // 변수를 선언할거에요 -> 기본 물리 데미지를 baseDamage에 (이게 빠져서 오류남)
|
||||||
public float elementDamage; // 속성 추가 데미지
|
|
||||||
public float elementDuration; // 속성 지속시간 (초)
|
|
||||||
|
|
||||||
[Header("--- 프리팹 ---")]
|
[Header("속성 정보")] // 인스펙터 창에 제목을 표시할거에요 -> 속성 정보 를
|
||||||
public GameObject projectilePrefab; // 발사할 파티클 프리팹
|
public ArrowElementType elementType; // 변수를 선언할거에요 -> 속성 타입을
|
||||||
|
public float elementDamage; // 변수를 선언할거에요 -> 속성 추가 데미지를
|
||||||
/// <summary>
|
public float elementDuration; // 변수를 선언할거에요 -> 속성 지속 시간을
|
||||||
/// 기본 화살 데이터 생성
|
|
||||||
/// </summary>
|
|
||||||
public static ArrowData Default => new ArrowData
|
|
||||||
{
|
|
||||||
arrowName = "기본 화살",
|
|
||||||
elementType = ArrowElementType.None,
|
|
||||||
baseDamage = 10f,
|
|
||||||
elementDamage = 0f,
|
|
||||||
elementDuration = 0f,
|
|
||||||
projectilePrefab = null
|
|
||||||
};
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"[{arrowName}] 속성={elementType}, " +
|
|
||||||
$"기본데미지={baseDamage}, 속성데미지={elementDamage}, " +
|
|
||||||
$"지속시간={elementDuration}s";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,102 +1,102 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using System.Collections;
|
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
|
||||||
|
|
||||||
public class PlayerAttack : MonoBehaviour
|
public class PlayerAttack : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerAttack을
|
||||||
{
|
{
|
||||||
[Header("--- 활 설정 ---")]
|
[Header("--- 활 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 활 설정 --- 을
|
||||||
[SerializeField] private Transform firePoint;
|
[SerializeField] private Transform firePoint; // 변수를 선언할거에요 -> 화살 발사 위치인 firePoint를
|
||||||
[SerializeField] private PlayerAnimator pAnim;
|
[SerializeField] private PlayerAnimator pAnim; // 변수를 선언할거에요 -> 플레이어 애니메이터 스크립트인 pAnim을
|
||||||
|
|
||||||
[Header("--- 스탯 참조 ---")]
|
[Header("--- 스탯 참조 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 스탯 참조 --- 를
|
||||||
[SerializeField] private Stats playerStats;
|
[SerializeField] private Stats playerStats; // 변수를 선언할거에요 -> 플레이어 스탯 스크립트인 playerStats를
|
||||||
|
|
||||||
[Header("--- 일반 공격 (좌클릭) ---")]
|
[Header("--- 일반 공격 (좌클릭) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 일반 공격 (좌클릭) --- 을
|
||||||
[SerializeField] private float normalRange = 15f;
|
[SerializeField] private float normalRange = 15f; // 변수를 선언할거에요 -> 일반 공격 사거리인 normalRange를
|
||||||
[SerializeField] private float normalSpeed = 20f;
|
[SerializeField] private float normalSpeed = 20f; // 변수를 선언할거에요 -> 일반 화살 속도인 normalSpeed를
|
||||||
[SerializeField] private float attackCooldown = 0.5f;
|
[SerializeField] private float attackCooldown = 0.5f; // 변수를 선언할거에요 -> 공격 쿨타임인 attackCooldown을
|
||||||
|
|
||||||
[Header("--- 차징 공격 (우클릭) ---")]
|
[Header("--- 차징 공격 (우클릭) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 차징 공격 (우클릭) --- 을
|
||||||
[SerializeField] private float maxChargeTime = 2.0f;
|
[SerializeField] private float maxChargeTime = 2.0f; // 변수를 선언할거에요 -> 최대 차징 시간인 maxChargeTime을
|
||||||
|
|
||||||
[System.Serializable]
|
[System.Serializable] // 직렬화할거에요 -> 인스펙터에서 수정할 수 있게 구조체를
|
||||||
public struct ChargeStage
|
public struct ChargeStage // 구조체를 정의할거에요 -> 차징 단계를 정의하는 ChargeStage를
|
||||||
{
|
{
|
||||||
public float chargeTime;
|
public float chargeTime; // 변수를 선언할거에요 -> 도달 시간인 chargeTime을
|
||||||
public float damageMult;
|
public float damageMult; // 변수를 선언할거에요 -> 데미지 배율인 damageMult를
|
||||||
public float rangeMult;
|
public float rangeMult; // 변수를 선언할거에요 -> 사거리/속도 배율인 rangeMult를
|
||||||
}
|
}
|
||||||
|
|
||||||
[SerializeField] private List<ChargeStage> chargeStages;
|
[SerializeField] private List<ChargeStage> chargeStages; // 리스트를 선언할거에요 -> 차징 단계 목록인 chargeStages를
|
||||||
|
|
||||||
[Header("--- 에임 보정 설정 ---")]
|
[Header("--- 에임 보정 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 에임 보정 설정 --- 을
|
||||||
[SerializeField] private bool enableAutoAim = true;
|
[SerializeField] private bool enableAutoAim = true; // 변수를 선언할거에요 -> 자동 조준 사용 여부인 enableAutoAim을
|
||||||
[SerializeField] private float autoAimRange = 15f;
|
[SerializeField] private float autoAimRange = 15f; // 변수를 선언할거에요 -> 자동 조준 감지 거리인 autoAimRange를
|
||||||
[SerializeField] private float autoAimAngle = 45f;
|
[SerializeField] private float autoAimAngle = 45f; // 변수를 선언할거에요 -> 자동 조준 감지 각도인 autoAimAngle을
|
||||||
[Range(0f, 1f)]
|
[Range(0f, 1f)] // 슬라이더를 만들거에요 -> 0부터 1 사이의 값으로
|
||||||
[SerializeField] private float aimAssistStrength = 0.4f;
|
[SerializeField] private float aimAssistStrength = 0.4f; // 변수를 선언할거에요 -> 보정 강도인 aimAssistStrength를
|
||||||
[SerializeField] private LayerMask enemyLayer;
|
[SerializeField] private LayerMask enemyLayer; // 변수를 선언할거에요 -> 적 레이어인 enemyLayer를
|
||||||
[SerializeField] private bool onlyMaxCharge = true;
|
[SerializeField] private bool onlyMaxCharge = true; // 변수를 선언할거에요 -> 풀차징 때만 유도할지 여부인 onlyMaxCharge를
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// [NEW] 파티클 기반 화살 시스템
|
// [NEW] 파티클 기반 화살 시스템
|
||||||
// ============================================
|
// ============================================
|
||||||
[Header("--- 파티클 화살 시스템 ---")]
|
[Header("--- 파티클 화살 시스템 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 파티클 화살 시스템 --- 을
|
||||||
[SerializeField] private GameObject defaultProjectilePrefab; // 기본 발사체 프리팹
|
[SerializeField] private GameObject defaultProjectilePrefab; // 변수를 선언할거에요 -> 기본 발사체 프리팹인 defaultProjectilePrefab을
|
||||||
|
|
||||||
private GameObject _currentProjectilePrefab;
|
private GameObject _currentProjectilePrefab; // 변수를 선언할거에요 -> 현재 사용할 발사체 프리팹인 _currentProjectilePrefab을
|
||||||
private ArrowElementType _currentElementType = ArrowElementType.None;
|
private ArrowElementType _currentElementType = ArrowElementType.None; // 변수를 초기화할거에요 -> 현재 화살 속성인 _currentElementType을 없음으로
|
||||||
private float _currentElementDamage = 0f;
|
private float _currentElementDamage = 0f; // 변수를 초기화할거에요 -> 현재 속성 데미지인 _currentElementDamage를 0으로
|
||||||
private float _currentElementDuration = 0f;
|
private float _currentElementDuration = 0f; // 변수를 초기화할거에요 -> 현재 속성 지속 시간인 _currentElementDuration을 0으로
|
||||||
|
|
||||||
private float _lastAttackTime;
|
private float _lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간인 _lastAttackTime을
|
||||||
private float _chargeTimer;
|
private float _chargeTimer; // 변수를 선언할거에요 -> 차징 진행 시간인 _chargeTimer를
|
||||||
private bool _isCharging = false;
|
private bool _isCharging = false; // 변수를 초기화할거에요 -> 차징 중 여부인 _isCharging을 거짓으로
|
||||||
private bool _isAttacking = false;
|
private bool _isAttacking = false; // 변수를 초기화할거에요 -> 공격 동작 중 여부인 _isAttacking을 거짓으로
|
||||||
private bool _waitForRelease = false;
|
private bool _waitForRelease = false; // 변수를 초기화할거에요 -> 발사 대기 상태 여부인 _waitForRelease를 거짓으로
|
||||||
|
|
||||||
private float _pendingDamage;
|
private float _pendingDamage; // 변수를 선언할거에요 -> 발사될 화살의 데미지인 _pendingDamage를
|
||||||
private float _pendingSpeed;
|
private float _pendingSpeed; // 변수를 선언할거에요 -> 발사될 화살의 속도인 _pendingSpeed를
|
||||||
private float _pendingRange;
|
private float _pendingRange; // 변수를 선언할거에요 -> 발사될 화살의 사거리인 _pendingRange를
|
||||||
private Vector3 _pendingShootDirection;
|
private Vector3 _pendingShootDirection; // 변수를 선언할거에요 -> 발사될 방향인 _pendingShootDirection을
|
||||||
|
|
||||||
public bool IsCharging => _isCharging;
|
public bool IsCharging => _isCharging; // 프로퍼티를 선언할거에요 -> 차징 중인지 여부를 반환하는 IsCharging을
|
||||||
public bool IsAttacking { get => _isAttacking; set => _isAttacking = value; }
|
public bool IsAttacking { get => _isAttacking; set => _isAttacking = value; } // 프로퍼티를 선언할거에요 -> 공격 중인지 여부를 읽고 쓰는 IsAttacking을
|
||||||
public float ChargeProgress => Mathf.Clamp01(_chargeTimer / maxChargeTime);
|
public float ChargeProgress => Mathf.Clamp01(_chargeTimer / maxChargeTime); // 프로퍼티를 선언할거에요 -> 차징 진행률(0~1)을 반환하는 ChargeProgress를
|
||||||
|
|
||||||
// [NEW] 외부에서 현재 속성 확인용
|
// [NEW] 외부에서 현재 속성 확인용
|
||||||
public ArrowElementType CurrentElement => _currentElementType;
|
public ArrowElementType CurrentElement => _currentElementType; // 프로퍼티를 선언할거에요 -> 현재 화살 속성을 반환하는 CurrentElement를
|
||||||
public GameObject CurrentProjectilePrefab => _currentProjectilePrefab;
|
public GameObject CurrentProjectilePrefab => _currentProjectilePrefab; // 프로퍼티를 선언할거에요 -> 현재 발사체 프리팹을 반환하는 CurrentProjectilePrefab을
|
||||||
|
|
||||||
private void Start()
|
private void Start() // 함수를 실행할거에요 -> 스크립트 시작 시 Start를
|
||||||
{
|
{
|
||||||
if (playerStats == null) playerStats = GetComponentInParent<Stats>();
|
if (playerStats == null) playerStats = GetComponentInParent<Stats>(); // 조건이 맞으면 가져올거에요 -> 부모의 Stats 컴포넌트를 playerStats에
|
||||||
|
|
||||||
// 기본 발사체 프리팹 설정
|
// 기본 발사체 프리팹 설정
|
||||||
if (_currentProjectilePrefab == null)
|
if (_currentProjectilePrefab == null) // 조건이 맞으면 실행할거에요 -> 현재 설정된 프리팹이 없다면
|
||||||
_currentProjectilePrefab = defaultProjectilePrefab;
|
_currentProjectilePrefab = defaultProjectilePrefab; // 값을 넣을거에요 -> 기본 프리팹을
|
||||||
|
|
||||||
if (chargeStages == null || chargeStages.Count == 0)
|
if (chargeStages == null || chargeStages.Count == 0) // 조건이 맞으면 실행할거에요 -> 차징 단계 리스트가 비어있다면
|
||||||
{
|
{
|
||||||
chargeStages = new List<ChargeStage>
|
chargeStages = new List<ChargeStage> // 리스트를 생성할거에요 -> 기본 3단계 차징 설정을 담은 리스트를
|
||||||
{
|
{
|
||||||
new ChargeStage { chargeTime = 0f, damageMult = 1f, rangeMult = 1f },
|
new ChargeStage { chargeTime = 0f, damageMult = 1f, rangeMult = 1f }, // 값을 넣을거에요 -> 0단계 설정을
|
||||||
new ChargeStage { chargeTime = 1f, damageMult = 1.5f, rangeMult = 1.2f },
|
new ChargeStage { chargeTime = 1f, damageMult = 1.5f, rangeMult = 1.2f }, // 값을 넣을거에요 -> 1단계 설정을
|
||||||
new ChargeStage { chargeTime = 2f, damageMult = 2.5f, rangeMult = 1.5f }
|
new ChargeStage { chargeTime = 2f, damageMult = 2.5f, rangeMult = 1.5f } // 값을 넣을거에요 -> 2단계 설정을
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
float calculatedMaxTime = 0f;
|
float calculatedMaxTime = 0f; // 변수를 초기화할거에요 -> 계산된 최대 시간을 0으로
|
||||||
foreach (var stage in chargeStages)
|
foreach (var stage in chargeStages) // 반복할거에요 -> 모든 차징 단계에 대해
|
||||||
{
|
{
|
||||||
if (stage.chargeTime > calculatedMaxTime) calculatedMaxTime = stage.chargeTime;
|
if (stage.chargeTime > calculatedMaxTime) calculatedMaxTime = stage.chargeTime; // 조건이 맞으면 갱신할거에요 -> 더 긴 시간이 있다면 그 값으로
|
||||||
}
|
}
|
||||||
maxChargeTime = Mathf.Max(calculatedMaxTime, 0.1f);
|
maxChargeTime = Mathf.Max(calculatedMaxTime, 0.1f); // 값을 설정할거에요 -> 계산된 최대 시간으로 (최소 0.1 보장)
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를
|
||||||
{
|
{
|
||||||
if (_isCharging) _chargeTimer += Time.deltaTime;
|
if (_isCharging) _chargeTimer += Time.deltaTime; // 조건이 맞으면 더할거에요 -> 차징 중이라면 타이머에 시간을
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
@ -107,165 +107,165 @@ public class PlayerAttack : MonoBehaviour
|
||||||
/// ArrowPickup에서 습득 시 호출합니다.
|
/// ArrowPickup에서 습득 시 호출합니다.
|
||||||
/// 파티클 프리팹 + 속성 정보를 저장합니다.
|
/// 파티클 프리팹 + 속성 정보를 저장합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SetCurrentArrow(ArrowData data)
|
public void SetCurrentArrow(ArrowData data) // 함수를 선언할거에요 -> 새로운 화살 데이터를 적용하는 SetCurrentArrow를
|
||||||
{
|
{
|
||||||
if (data.projectilePrefab != null)
|
if (data.projectilePrefab != null) // 조건이 맞으면 실행할거에요 -> 데이터에 프리팹이 있다면
|
||||||
_currentProjectilePrefab = data.projectilePrefab;
|
_currentProjectilePrefab = data.projectilePrefab; // 값을 바꿀거에요 -> 현재 발사체 프리팹을 새 것으로
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 데이터에 프리팹이 없다면
|
||||||
_currentProjectilePrefab = defaultProjectilePrefab;
|
_currentProjectilePrefab = defaultProjectilePrefab; // 값을 바꿀거에요 -> 기본 프리팹으로
|
||||||
|
|
||||||
_currentElementType = data.elementType;
|
_currentElementType = data.elementType; // 값을 저장할거에요 -> 화살 속성 타입을
|
||||||
_currentElementDamage = data.elementDamage;
|
_currentElementDamage = data.elementDamage; // 값을 저장할거에요 -> 속성 데미지를
|
||||||
_currentElementDuration = data.elementDuration;
|
_currentElementDuration = data.elementDuration; // 값을 저장할거에요 -> 속성 지속 시간을
|
||||||
|
|
||||||
Debug.Log($"화살 장착: [{data.arrowName}] 속성={data.elementType}, " +
|
Debug.Log($"화살 장착: [{data.arrowName}] 속성={data.elementType}, " +
|
||||||
$"속성데미지={data.elementDamage}, 지속시간={data.elementDuration}s");
|
$"속성데미지={data.elementDamage}, 지속시간={data.elementDuration}s"); // 로그를 출력할거에요 -> 장착된 화살 정보를
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 기본 화살로 초기화
|
/// 기본 화살로 초기화
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ResetArrow()
|
public void ResetArrow() // 함수를 선언할거에요 -> 화살을 기본 상태로 되돌리는 ResetArrow를
|
||||||
{
|
{
|
||||||
_currentProjectilePrefab = defaultProjectilePrefab;
|
_currentProjectilePrefab = defaultProjectilePrefab; // 값을 바꿀거에요 -> 발사체를 기본 프리팹으로
|
||||||
_currentElementType = ArrowElementType.None;
|
_currentElementType = ArrowElementType.None; // 값을 바꿀거에요 -> 속성을 없음(None)으로
|
||||||
_currentElementDamage = 0f;
|
_currentElementDamage = 0f; // 값을 초기화할거에요 -> 속성 데미지를 0으로
|
||||||
_currentElementDuration = 0f;
|
_currentElementDuration = 0f; // 값을 초기화할거에요 -> 속성 시간을 0으로
|
||||||
Debug.Log("화살 초기화: 기본 화살");
|
Debug.Log("화살 초기화: 기본 화살"); // 로그를 출력할거에요 -> 초기화 완료 메시지를
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 일반 공격 (좌클릭) — 기존 로직 100% 유지
|
// 일반 공격 (좌클릭) — 기존 로직 100% 유지
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
public void PerformNormalAttack()
|
public void PerformNormalAttack() // 함수를 선언할거에요 -> 일반 공격을 수행하는 PerformNormalAttack을
|
||||||
{
|
{
|
||||||
if (Time.time < _lastAttackTime + attackCooldown || _isAttacking) return;
|
if (Time.time < _lastAttackTime + attackCooldown || _isAttacking) return; // 조건이 맞으면 중단할거에요 -> 쿨타임 중이거나 이미 공격 중이라면
|
||||||
|
|
||||||
_pendingDamage = (playerStats != null) ? playerStats.TotalAttackDamage : 10f;
|
_pendingDamage = (playerStats != null) ? playerStats.TotalAttackDamage : 10f; // 값을 가져올거에요 -> 스탯이 있으면 총 공격력을, 없으면 10을
|
||||||
_pendingSpeed = normalSpeed;
|
_pendingSpeed = normalSpeed; // 값을 저장할거에요 -> 기본 속도를
|
||||||
_pendingRange = normalRange;
|
_pendingRange = normalRange; // 값을 저장할거에요 -> 기본 사거리를
|
||||||
|
|
||||||
_pendingShootDirection = GetMouseDirection();
|
_pendingShootDirection = GetMouseDirection(); // 값을 계산할거에요 -> 마우스 방향을 구해서 발사 방향으로
|
||||||
_lastAttackTime = Time.time;
|
_lastAttackTime = Time.time; // 값을 갱신할거에요 -> 마지막 공격 시간을 현재로
|
||||||
|
|
||||||
if (pAnim != null) pAnim.TriggerThrow();
|
if (pAnim != null) pAnim.TriggerThrow(); // 실행할거에요 -> 던지는 애니메이션 트리거를
|
||||||
StartCoroutine(AttackRoutine());
|
StartCoroutine(AttackRoutine()); // 코루틴을 시작할거에요 -> 공격 상태 관리를 위한 AttackRoutine을
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 차징 공격 (우클릭) — 기존 로직 100% 유지
|
// 차징 공격 (우클릭) — 기존 로직 100% 유지
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
public void StartCharging()
|
public void StartCharging() // 함수를 선언할거에요 -> 차징을 시작하는 StartCharging을
|
||||||
{
|
{
|
||||||
if (_waitForRelease) return;
|
if (_waitForRelease) return; // 조건이 맞으면 중단할거에요 -> 이미 발사 대기 중이라면
|
||||||
_isCharging = true;
|
_isCharging = true; // 상태를 바꿀거에요 -> 차징 중 상태를 참(true)으로
|
||||||
_chargeTimer = 0f;
|
_chargeTimer = 0f; // 값을 초기화할거에요 -> 차징 타이머를 0으로
|
||||||
if (pAnim != null) pAnim.SetCharging(true);
|
if (pAnim != null) pAnim.SetCharging(true); // 실행할거에요 -> 애니메이터에 차징 시작을 알리기를
|
||||||
if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(true);
|
if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(true); // 조건이 맞으면 실행할거에요 -> 카메라 줌 효과를 켜기를
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResetChargingEffects()
|
private void ResetChargingEffects() // 함수를 선언할거에요 -> 차징 효과를 초기화하는 ResetChargingEffects를
|
||||||
{
|
{
|
||||||
_isCharging = false;
|
_isCharging = false; // 상태를 바꿀거에요 -> 차징 상태를 거짓(false)으로
|
||||||
_chargeTimer = 0f;
|
_chargeTimer = 0f; // 값을 초기화할거에요 -> 차징 타이머를
|
||||||
if (pAnim != null) pAnim.SetCharging(false);
|
if (pAnim != null) pAnim.SetCharging(false); // 실행할거에요 -> 애니메이터 차징 상태 끄기를
|
||||||
if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(false);
|
if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(false); // 조건이 맞으면 실행할거에요 -> 카메라 줌 효과를 끄기를
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CancelCharging()
|
public void CancelCharging() // 함수를 선언할거에요 -> 차징을 취소하는 CancelCharging을
|
||||||
{
|
{
|
||||||
ResetChargingEffects();
|
ResetChargingEffects(); // 함수를 실행할거에요 -> 차징 효과 초기화를
|
||||||
_waitForRelease = false;
|
_waitForRelease = false; // 상태를 바꿀거에요 -> 발사 대기 상태를 거짓으로
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ReleaseAttack()
|
public void ReleaseAttack() // 함수를 선언할거에요 -> 차징된 공격을 발사하는 ReleaseAttack을
|
||||||
{
|
{
|
||||||
if (!_isCharging) return;
|
if (!_isCharging) return; // 조건이 맞으면 중단할거에요 -> 차징 중이 아니라면
|
||||||
|
|
||||||
ChargeStage currentStage = chargeStages[0];
|
ChargeStage currentStage = chargeStages[0]; // 변수를 초기화할거에요 -> 기본 0단계를 시작값으로
|
||||||
foreach (var stage in chargeStages)
|
foreach (var stage in chargeStages) // 반복할거에요 -> 모든 단계를 돌면서
|
||||||
{
|
{
|
||||||
if (_chargeTimer >= stage.chargeTime) currentStage = stage;
|
if (_chargeTimer >= stage.chargeTime) currentStage = stage; // 조건이 맞으면 갱신할거에요 -> 타이머가 단계 시간보다 길다면 해당 단계로
|
||||||
}
|
}
|
||||||
|
|
||||||
float baseDmg = (playerStats != null) ? playerStats.TotalAttackDamage : 10f;
|
float baseDmg = (playerStats != null) ? playerStats.TotalAttackDamage : 10f; // 값을 가져올거에요 -> 기본 공격력을
|
||||||
_pendingDamage = baseDmg * currentStage.damageMult;
|
_pendingDamage = baseDmg * currentStage.damageMult; // 값을 계산할거에요 -> 배율을 적용한 최종 데미지를
|
||||||
_pendingSpeed = normalSpeed * currentStage.rangeMult;
|
_pendingSpeed = normalSpeed * currentStage.rangeMult; // 값을 계산할거에요 -> 배율을 적용한 속도를
|
||||||
_pendingRange = normalRange * currentStage.rangeMult;
|
_pendingRange = normalRange * currentStage.rangeMult; // 값을 계산할거에요 -> 배율을 적용한 사거리를
|
||||||
|
|
||||||
bool isMaxCharge = _chargeTimer >= maxChargeTime * 0.95f;
|
bool isMaxCharge = _chargeTimer >= maxChargeTime * 0.95f; // 조건을 확인할거에요 -> 풀차징(95% 이상)인지 여부를
|
||||||
_pendingShootDirection = GetShootDirection(isMaxCharge);
|
_pendingShootDirection = GetShootDirection(isMaxCharge); // 값을 계산할거에요 -> 보정이 적용된 발사 방향을
|
||||||
|
|
||||||
if (pAnim != null) pAnim.TriggerThrow();
|
if (pAnim != null) pAnim.TriggerThrow(); // 실행할거에요 -> 던지는 애니메이션을
|
||||||
_waitForRelease = true;
|
_waitForRelease = true; // 상태를 바꿀거에요 -> 발사 대기 상태를 참으로
|
||||||
_lastAttackTime = Time.time;
|
_lastAttackTime = Time.time; // 값을 갱신할거에요 -> 마지막 공격 시간을
|
||||||
StartCoroutine(AttackRoutine());
|
StartCoroutine(AttackRoutine()); // 코루틴을 시작할거에요 -> 공격 루틴을
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 에임 보정 시스템 — 기존 로직 100% 유지
|
// 에임 보정 시스템 — 기존 로직 100% 유지
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
private Vector3 GetMouseDirection()
|
private Vector3 GetMouseDirection() // 함수를 선언할거에요 -> 마우스가 가리키는 방향을 구하는 GetMouseDirection을
|
||||||
{
|
{
|
||||||
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
|
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); // 레이를 쏠거에요 -> 카메라에서 마우스 위치로
|
||||||
Plane firePlane = new Plane(Vector3.up, firePoint.position);
|
Plane firePlane = new Plane(Vector3.up, firePoint.position); // 평면을 만들거에요 -> 발사 위치 높이의 수평면을
|
||||||
|
|
||||||
if (firePlane.Raycast(ray, out float distance))
|
if (firePlane.Raycast(ray, out float distance)) // 조건이 맞으면 실행할거에요 -> 레이가 평면에 닿았다면
|
||||||
{
|
{
|
||||||
Vector3 worldMousePos = ray.GetPoint(distance);
|
Vector3 worldMousePos = ray.GetPoint(distance); // 위치를 구할거에요 -> 평면상 마우스 위치를
|
||||||
Vector3 direction = (worldMousePos - firePoint.position).normalized;
|
Vector3 direction = (worldMousePos - firePoint.position).normalized; // 벡터를 구할거에요 -> 발사 위치에서 마우스까지의 방향을
|
||||||
direction.y = 0;
|
direction.y = 0; // 값을 바꿀거에요 -> 높이 차이는 무시하게 y를 0으로
|
||||||
return direction;
|
return direction; // 반환할거에요 -> 계산된 방향을
|
||||||
}
|
}
|
||||||
return transform.forward;
|
return transform.forward; // 반환할거에요 -> 실패 시 정면 방향을
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vector3 GetShootDirection(bool isMaxCharge)
|
private Vector3 GetShootDirection(bool isMaxCharge) // 함수를 선언할거에요 -> 최종 발사 방향(보정 포함)을 구하는 GetShootDirection을
|
||||||
{
|
{
|
||||||
Vector3 mouseDir = GetMouseDirection();
|
Vector3 mouseDir = GetMouseDirection(); // 값을 가져올거에요 -> 마우스 방향을
|
||||||
if (!enableAutoAim || (onlyMaxCharge && !isMaxCharge)) return mouseDir;
|
if (!enableAutoAim || (onlyMaxCharge && !isMaxCharge)) return mouseDir; // 조건이 맞으면 반환할거에요 -> 보정 안 씀 설정이거나 풀차징이 아니면 마우스 방향 그대로
|
||||||
|
|
||||||
Transform bestTarget = FindBestTarget(mouseDir);
|
Transform bestTarget = FindBestTarget(mouseDir); // 함수를 실행할거에요 -> 가장 적합한 타겟을 찾는 FindBestTarget을
|
||||||
if (bestTarget != null)
|
if (bestTarget != null) // 조건이 맞으면 실행할거에요 -> 타겟이 있다면
|
||||||
{
|
{
|
||||||
Vector3 targetPos = bestTarget.position + Vector3.up * 1.2f;
|
Vector3 targetPos = bestTarget.position + Vector3.up * 1.2f; // 위치를 계산할거에요 -> 타겟의 중심부(높이 보정)를
|
||||||
Vector3 targetDir = (targetPos - firePoint.position).normalized;
|
Vector3 targetDir = (targetPos - firePoint.position).normalized; // 방향을 계산할거에요 -> 발사 위치에서 타겟까지
|
||||||
targetDir.y = 0;
|
targetDir.y = 0; // 값을 바꿀거에요 -> 수평 방향만 고려하게
|
||||||
|
|
||||||
Vector3 assistedDir = Vector3.Lerp(mouseDir, targetDir, aimAssistStrength);
|
Vector3 assistedDir = Vector3.Lerp(mouseDir, targetDir, aimAssistStrength); // 보간할거에요 -> 마우스 방향과 타겟 방향 사이를 강도만큼
|
||||||
assistedDir.Normalize();
|
assistedDir.Normalize(); // 정규화할거에요 -> 벡터 길이를 1로
|
||||||
return assistedDir;
|
return assistedDir; // 반환할거에요 -> 보정된 방향을
|
||||||
}
|
}
|
||||||
return mouseDir;
|
return mouseDir; // 반환할거에요 -> 타겟 없으면 마우스 방향을
|
||||||
}
|
}
|
||||||
|
|
||||||
private Transform FindBestTarget(Vector3 mouseDirection)
|
private Transform FindBestTarget(Vector3 mouseDirection) // 함수를 선언할거에요 -> 조준 보정 대상을 찾는 FindBestTarget을
|
||||||
{
|
{
|
||||||
Collider[] enemies = Physics.OverlapSphere(transform.position, autoAimRange, enemyLayer);
|
Collider[] enemies = Physics.OverlapSphere(transform.position, autoAimRange, enemyLayer); // 배열에 담을거에요 -> 사거리 내의 모든 적을
|
||||||
if (enemies.Length == 0) return null;
|
if (enemies.Length == 0) return null; // 조건이 맞으면 반환할거에요 -> 적이 없으면 null을
|
||||||
|
|
||||||
Transform bestTarget = null;
|
Transform bestTarget = null; // 변수를 초기화할거에요 -> 최고 타겟을 비워두고
|
||||||
float bestScore = float.MaxValue;
|
float bestScore = float.MaxValue; // 변수를 초기화할거에요 -> 점수를 최대값으로
|
||||||
|
|
||||||
foreach (var enemy in enemies)
|
foreach (var enemy in enemies) // 반복할거에요 -> 모든 적에 대해
|
||||||
{
|
{
|
||||||
Vector3 dirToEnemy = (enemy.transform.position - transform.position).normalized;
|
Vector3 dirToEnemy = (enemy.transform.position - transform.position).normalized; // 방향을 구할거에요 -> 적을 향하는 방향을
|
||||||
float angle = Vector3.Angle(mouseDirection, dirToEnemy);
|
float angle = Vector3.Angle(mouseDirection, dirToEnemy); // 각도를 잴거에요 -> 마우스 방향과 적 방향 사이를
|
||||||
if (angle > autoAimAngle) continue;
|
if (angle > autoAimAngle) continue; // 조건이 맞으면 건너뛸거에요 -> 시야각 밖이라면
|
||||||
|
|
||||||
float distance = Vector3.Distance(transform.position, enemy.transform.position);
|
float distance = Vector3.Distance(transform.position, enemy.transform.position); // 거리를 잴거에요 -> 적까지의 거리를
|
||||||
float score = angle * 0.5f + distance * 0.5f;
|
float score = angle * 0.5f + distance * 0.5f; // 점수를 매길거에요 -> 각도와 거리를 합산해서 (낮을수록 좋음)
|
||||||
|
|
||||||
if (score < bestScore)
|
if (score < bestScore) // 조건이 맞으면 갱신할거에요 -> 현재 점수가 최고 점수보다 낮다면
|
||||||
{
|
{
|
||||||
bestScore = score;
|
bestScore = score; // 값을 저장할거에요 -> 새 점수를
|
||||||
bestTarget = enemy.transform;
|
bestTarget = enemy.transform; // 값을 저장할거에요 -> 새 타겟을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bestTarget;
|
return bestTarget; // 반환할거에요 -> 가장 점수가 좋은 타겟을
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
@ -276,27 +276,27 @@ public class PlayerAttack : MonoBehaviour
|
||||||
/// 애니메이션 이벤트에서 호출됩니다.
|
/// 애니메이션 이벤트에서 호출됩니다.
|
||||||
/// 파티클 프리팹을 Instantiate하고 PlayerArrow 컴포넌트로 이동/충돌을 처리합니다.
|
/// 파티클 프리팹을 Instantiate하고 PlayerArrow 컴포넌트로 이동/충돌을 처리합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void OnShootArrow()
|
public void OnShootArrow() // 함수를 선언할거에요 -> 애니메이션 이벤트로 호출될 발사 함수 OnShootArrow를
|
||||||
{
|
{
|
||||||
if (_currentProjectilePrefab == null || firePoint == null)
|
if (_currentProjectilePrefab == null || firePoint == null) // 조건이 맞으면 중단할거에요 -> 프리팹이나 발사 위치가 없다면
|
||||||
{
|
{
|
||||||
Debug.LogWarning("발사체 프리팹 또는 firePoint가 없습니다!");
|
Debug.LogWarning("발사체 프리팹 또는 firePoint가 없습니다!"); // 경고 로그를 띄울거에요 -> 설정 확인 필요 메시지를
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 파티클 프리팹을 발사 위치에 생성
|
// 파티클 프리팹을 발사 위치에 생성
|
||||||
Quaternion rotation = Quaternion.LookRotation(_pendingShootDirection);
|
Quaternion rotation = Quaternion.LookRotation(_pendingShootDirection); // 회전을 계산할거에요 -> 발사 방향을 바라보게
|
||||||
GameObject projectile = Instantiate(_currentProjectilePrefab, firePoint.position, rotation);
|
GameObject projectile = Instantiate(_currentProjectilePrefab, firePoint.position, rotation); // 생성할거에요 -> 화살 오브젝트를
|
||||||
|
|
||||||
// PlayerArrow 컴포넌트 확인/추가 후 초기화
|
// PlayerArrow 컴포넌트 확인/추가 후 초기화
|
||||||
PlayerArrow arrowScript = projectile.GetComponent<PlayerArrow>();
|
PlayerArrow arrowScript = projectile.GetComponent<PlayerArrow>(); // 컴포넌트를 가져올거에요 -> PlayerArrow 스크립트를
|
||||||
if (arrowScript == null)
|
if (arrowScript == null) // 조건이 맞으면 실행할거에요 -> 스크립트가 없다면
|
||||||
{
|
{
|
||||||
arrowScript = projectile.AddComponent<PlayerArrow>();
|
arrowScript = projectile.AddComponent<PlayerArrow>(); // 추가할거에요 -> PlayerArrow 컴포넌트를 즉석에서
|
||||||
}
|
}
|
||||||
|
|
||||||
// 발사 정보 + 속성 정보 전달
|
// 발사 정보 + 속성 정보 전달
|
||||||
arrowScript.Initialize(
|
arrowScript.Initialize( // 초기화할거에요 -> 화살 스크립트에 모든 정보를 전달해서
|
||||||
_pendingDamage,
|
_pendingDamage,
|
||||||
_pendingSpeed,
|
_pendingSpeed,
|
||||||
_pendingRange,
|
_pendingRange,
|
||||||
|
|
@ -311,63 +311,63 @@ public class PlayerAttack : MonoBehaviour
|
||||||
// 유틸리티 — 기존 로직 100% 유지
|
// 유틸리티 — 기존 로직 100% 유지
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
public void OnAttackEnd()
|
public void OnAttackEnd() // 함수를 선언할거에요 -> 공격 애니메이션 종료 시 호출될 OnAttackEnd를
|
||||||
{
|
{
|
||||||
_isAttacking = false;
|
_isAttacking = false; // 상태를 바꿀거에요 -> 공격 상태를 거짓으로
|
||||||
ResetChargingEffects();
|
ResetChargingEffects(); // 함수를 실행할거에요 -> 차징 효과 초기화를
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerator AttackRoutine()
|
private IEnumerator AttackRoutine() // 코루틴 함수를 선언할거에요 -> 공격 상태 안전장치인 AttackRoutine을
|
||||||
{
|
{
|
||||||
_isAttacking = true;
|
_isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참으로
|
||||||
yield return new WaitForSeconds(0.6f);
|
yield return new WaitForSeconds(0.6f); // 기다릴거에요 -> 0.6초(모션 시간)만큼
|
||||||
if (_isAttacking)
|
if (_isAttacking) // 조건이 맞으면 실행할거에요 -> 아직도 공격 중 상태라면 (강제 종료)
|
||||||
{
|
{
|
||||||
_isAttacking = false;
|
_isAttacking = false; // 상태를 바꿀거에요 -> 공격 상태를 거짓으로
|
||||||
ResetChargingEffects();
|
ResetChargingEffects(); // 함수를 실행할거에요 -> 효과 초기화를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// [DEPRECATED] 하위 호환성을 위해 남겨둠
|
// [DEPRECATED] 하위 호환성을 위해 남겨둠
|
||||||
[System.Obsolete("SetCurrentArrow(ArrowData)를 사용하세요.")]
|
[System.Obsolete("SetCurrentArrow(ArrowData)를 사용하세요.")] // 경고를 띄울거에요 -> 이 함수는 더 이상 쓰지 말라고
|
||||||
public void SwapArrow(GameObject newArrow)
|
public void SwapArrow(GameObject newArrow) // 함수를 선언할거에요 -> 구버전 화살 교체 함수 SwapArrow를
|
||||||
{
|
{
|
||||||
if (newArrow == null) return;
|
if (newArrow == null) return; // 조건이 맞으면 중단할거에요 -> 비어있다면
|
||||||
Debug.LogWarning("SwapArrow()는 더 이상 사용되지 않습니다. SetCurrentArrow()를 사용하세요.");
|
Debug.LogWarning("SwapArrow()는 더 이상 사용되지 않습니다. SetCurrentArrow()를 사용하세요."); // 경고 로그를 출력할거에요 -> 새 함수 사용 권장을
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StartWeaponCollision() { }
|
public void StartWeaponCollision() { } // 함수를 비워둘거에요 -> 근접 무기 충돌 시작(활에는 필요 없음)
|
||||||
public void StopWeaponCollision() { }
|
public void StopWeaponCollision() { } // 함수를 비워둘거에요 -> 근접 무기 충돌 종료(활에는 필요 없음)
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Gizmo 디버그 — 기존 로직 100% 유지
|
// Gizmo 디버그 — 기존 로직 100% 유지
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
private void OnDrawGizmosSelected()
|
private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 에디터 선택 시 디버그 그림을 그리는 OnDrawGizmosSelected를
|
||||||
{
|
{
|
||||||
if (!enableAutoAim) return;
|
if (!enableAutoAim) return; // 조건이 맞으면 중단할거에요 -> 자동 조준이 꺼져있다면
|
||||||
Gizmos.color = Color.yellow;
|
Gizmos.color = Color.yellow; // 색상을 설정할거에요 -> 노란색으로
|
||||||
Gizmos.DrawWireSphere(transform.position, autoAimRange);
|
Gizmos.DrawWireSphere(transform.position, autoAimRange); // 그림을 그릴거에요 -> 자동 조준 범위를
|
||||||
|
|
||||||
if (Application.isPlaying && firePoint != null)
|
if (Application.isPlaying && firePoint != null) // 조건이 맞으면 실행할거에요 -> 게임 실행 중이고 발사 위치가 있다면
|
||||||
{
|
{
|
||||||
Vector3 mouseDir = GetMouseDirection();
|
Vector3 mouseDir = GetMouseDirection(); // 값을 가져올거에요 -> 마우스 방향을
|
||||||
Gizmos.color = Color.blue;
|
Gizmos.color = Color.blue; // 색상을 설정할거에요 -> 파란색으로
|
||||||
Gizmos.DrawRay(firePoint.position, mouseDir * 5f);
|
Gizmos.DrawRay(firePoint.position, mouseDir * 5f); // 선을 그릴거에요 -> 마우스 방향 레이를
|
||||||
|
|
||||||
Transform target = FindBestTarget(mouseDir);
|
Transform target = FindBestTarget(mouseDir); // 값을 가져올거에요 -> 타겟을
|
||||||
if (target != null)
|
if (target != null) // 조건이 맞으면 실행할거에요 -> 타겟이 있다면
|
||||||
{
|
{
|
||||||
Vector3 targetPos = target.position + Vector3.up * 1.2f;
|
Vector3 targetPos = target.position + Vector3.up * 1.2f; // 위치를 계산할거에요 -> 타겟 중심점을
|
||||||
Vector3 targetDir = (targetPos - firePoint.position).normalized;
|
Vector3 targetDir = (targetPos - firePoint.position).normalized; // 방향을 계산할거에요 -> 타겟 방향을
|
||||||
targetDir.y = 0;
|
targetDir.y = 0; // 값을 바꿀거에요 -> 수평으로
|
||||||
Vector3 assistedDir = Vector3.Lerp(mouseDir, targetDir, aimAssistStrength);
|
Vector3 assistedDir = Vector3.Lerp(mouseDir, targetDir, aimAssistStrength); // 보간할거에요 -> 보정된 방향을
|
||||||
assistedDir.Normalize();
|
assistedDir.Normalize(); // 정규화할거에요 -> 벡터를
|
||||||
|
|
||||||
Gizmos.color = Color.red;
|
Gizmos.color = Color.red; // 색상을 설정할거에요 -> 빨간색으로
|
||||||
Gizmos.DrawRay(firePoint.position, assistedDir * 7f);
|
Gizmos.DrawRay(firePoint.position, assistedDir * 7f); // 선을 그릴거에요 -> 최종 발사 방향을
|
||||||
Gizmos.color = Color.green;
|
Gizmos.color = Color.green; // 색상을 설정할거에요 -> 초록색으로
|
||||||
Gizmos.DrawWireSphere(targetPos, 0.3f);
|
Gizmos.DrawWireSphere(targetPos, 0.3f); // 그림을 그릴거에요 -> 타겟 위치 표시를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum StatusEffectType
|
public enum StatusEffectType
|
||||||
{
|
{
|
||||||
|
None, // ⭐ [추가됨] 아무 상태이상 없음 (기본값)
|
||||||
Burn, // 화상 - 일정 시간 동안 틱 데미지
|
Burn, // 화상 - 일정 시간 동안 틱 데미지
|
||||||
Slow, // 슬로우 - 이동속도 감소
|
Slow, // 슬로우 - 이동속도 감소
|
||||||
Poison, // 중독 - 틱 데미지 + 방어력 감소
|
Poison, // 중독 - 틱 데미지 + 방어력 감소
|
||||||
|
|
|
||||||
|
|
@ -1,228 +1,229 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
|
||||||
|
|
||||||
/// <summary>
|
/// <summary> // 요약 주석을 시작할거에요 -> 클래스 설명을
|
||||||
/// 바닥에 떨어진 화살 아이템 (아이템 전용 — 발사체 기능 제거됨)
|
/// 바닥에 떨어진 화살 아이템 (아이템 전용 — 발사체 기능 제거됨) // 설명할거에요 -> 이 스크립트 역할을
|
||||||
/// 습득 시 PlayerAttack에 파티클 프리팹 + 속성 정보를 전달합니다.
|
/// 습득 시 PlayerAttack에 파티클 프리팹 + 속성 정보를 전달합니다. // 설명할거에요 -> 습득 시 전달 내용
|
||||||
/// </summary>
|
/// </summary> // 요약 주석을 끝낼거에요 -> 클래스 설명을
|
||||||
public class ArrowPickup : MonoBehaviour
|
public class ArrowPickup : MonoBehaviour // 클래스를 선언할거에요 -> 화살 아이템 습득/표시를 담당하는 ArrowPickup을
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> ArrowPickup 범위를
|
||||||
[Header("--- 화살 아이템 정보 ---")]
|
|
||||||
[SerializeField] private string arrowName = "기본 화살";
|
|
||||||
[SerializeField] private ArrowElementType elementType = ArrowElementType.None;
|
|
||||||
|
|
||||||
[Header("--- 스탯 ---")]
|
[Header("--- 화살 아이템 정보 ---")] // 인스펙터에 제목을 표시할거에요 -> 화살 아이템 정보 섹션을
|
||||||
[SerializeField] private float baseDamage = 15f;
|
[SerializeField] private string arrowName = "기본 화살"; // 변수를 선언할거에요 -> 화살 이름을 arrowName에
|
||||||
[SerializeField] private float elementDamage = 5f;
|
[SerializeField] private ArrowElementType elementType = ArrowElementType.None; // 변수를 선언할거에요 -> 화살 속성 타입을 elementType에
|
||||||
[SerializeField] private float elementDuration = 3f;
|
|
||||||
|
|
||||||
[Header("--- 발사체 파티클 ---")]
|
[Header("--- 스탯 ---")] // 인스펙터에 제목을 표시할거에요 -> 스탯 섹션을
|
||||||
[Tooltip("이 화살이 장착될 때 발사할 파티클 프리팹")]
|
[SerializeField] private float baseDamage = 15f; // 변수를 선언할거에요 -> 기본 데미지를 baseDamage에
|
||||||
[SerializeField] private GameObject projectilePrefab;
|
[SerializeField] private float elementDamage = 5f; // 변수를 선언할거에요 -> 속성 데미지를 elementDamage에
|
||||||
|
[SerializeField] private float elementDuration = 3f; // 변수를 선언할거에요 -> 속성 지속시간을 elementDuration에
|
||||||
|
|
||||||
[Header("--- 비주얼 (필드 아이템) ---")]
|
[Header("--- 발사체 파티클 ---")] // 인스펙터에 제목을 표시할거에요 -> 파티클 섹션을
|
||||||
[SerializeField] private GameObject visualModel;
|
[Tooltip("이 화살이 장착될 때 발사할 파티클 프리팹")] // 인스펙터에 툴팁을 달거에요 -> projectilePrefab 설명을
|
||||||
|
[SerializeField] private GameObject projectilePrefab; // 변수를 선언할거에요 -> 장착 시 쓸 파티클 프리팹을 projectilePrefab에
|
||||||
|
|
||||||
[Header("--- UI 표시 ---")]
|
[Header("--- 비주얼 (필드 아이템) ---")] // 인스펙터에 제목을 표시할거에요 -> 비주얼 섹션을
|
||||||
[SerializeField] private GameObject pickupUI;
|
[SerializeField] private GameObject visualModel; // 변수를 선언할거에요 -> 필드에서 보일 모델을 visualModel에
|
||||||
[SerializeField] private float uiDisplayRange = 3f;
|
|
||||||
|
|
||||||
[Header("--- Collider 설정 (자동 탐지) ---")]
|
[Header("--- UI 표시 ---")] // 인스펙터에 제목을 표시할거에요 -> UI 표시 섹션을
|
||||||
[Tooltip("아이템 습득용 Sphere Collider (Trigger)")]
|
[SerializeField] private GameObject pickupUI; // 변수를 선언할거에요 -> 습득 UI 오브젝트를 pickupUI에
|
||||||
[SerializeField] private Collider pickupCollider;
|
[SerializeField] private float uiDisplayRange = 3f; // 변수를 선언할거에요 -> UI 표시 거리를 uiDisplayRange에
|
||||||
[Tooltip("바닥 충돌용 Box Collider (물리)")]
|
|
||||||
[SerializeField] private Collider physicsCollider;
|
|
||||||
|
|
||||||
private Transform playerTransform;
|
[Header("--- Collider 설정 (자동 탐지) ---")] // 인스펙터에 제목을 표시할거에요 -> 콜라이더 설정 섹션을
|
||||||
private Rigidbody rb;
|
[Tooltip("아이템 습득용 Sphere Collider (Trigger)")] // 인스펙터에 툴팁을 달거에요 -> pickupCollider 설명을
|
||||||
|
[SerializeField] private Collider pickupCollider; // 변수를 선언할거에요 -> 습득용 트리거 콜라이더를 pickupCollider에
|
||||||
|
[Tooltip("바닥 충돌용 Box Collider (물리)")] // 인스펙터에 툴팁을 달거에요 -> physicsCollider 설명을
|
||||||
|
[SerializeField] private Collider physicsCollider; // 변수를 선언할거에요 -> 물리 충돌 콜라이더를 physicsCollider에
|
||||||
|
|
||||||
/// <summary>
|
private Transform playerTransform; // 변수를 선언할거에요 -> 플레이어 트랜스폼을 저장할 playerTransform을
|
||||||
/// [NEW] ArrowData 구조체로 정보 패키징
|
private Rigidbody rb; // 변수를 선언할거에요 -> 리지드바디를 저장할 rb를
|
||||||
/// </summary>
|
|
||||||
public ArrowData GetArrowData()
|
|
||||||
{
|
|
||||||
return new ArrowData
|
|
||||||
{
|
|
||||||
arrowName = this.arrowName,
|
|
||||||
elementType = this.elementType,
|
|
||||||
baseDamage = this.baseDamage,
|
|
||||||
elementDamage = this.elementDamage,
|
|
||||||
elementDuration = this.elementDuration,
|
|
||||||
projectilePrefab = this.projectilePrefab
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Awake()
|
/// <summary> // 요약 주석을 시작할거에요 -> ArrowData 패키징 설명을
|
||||||
{
|
/// [NEW] ArrowData 구조체로 정보 패키징 // 설명할거에요 -> PlayerAttack에 넘기기 쉽게 묶는 함수
|
||||||
rb = GetComponent<Rigidbody>();
|
/// </summary> // 요약 주석을 끝낼거에요 -> 설명 종료
|
||||||
|
public ArrowData GetArrowData() // 함수를 선언할거에요 -> 현재 화살 정보를 ArrowData로 반환하는 GetArrowData를
|
||||||
|
{ // 코드 블록을 시작할거에요 -> GetArrowData 범위를
|
||||||
|
return new ArrowData // 새 ArrowData를 만들어 반환할거에요 -> 화살 정보를 채워서
|
||||||
|
{ // 코드 블록을 시작할거에요 -> 초기화 블록 범위를
|
||||||
|
arrowName = this.arrowName, // 값을 넣을거에요 -> 화살 이름을
|
||||||
|
elementType = this.elementType, // 값을 넣을거에요 -> 속성 타입을
|
||||||
|
baseDamage = this.baseDamage, // 값을 넣을거에요 -> 기본 데미지를
|
||||||
|
elementDamage = this.elementDamage, // 값을 넣을거에요 -> 속성 데미지를
|
||||||
|
elementDuration = this.elementDuration, // 값을 넣을거에요 -> 속성 지속시간을
|
||||||
|
projectilePrefab = this.projectilePrefab // 값을 넣을거에요 -> 파티클 프리팹을
|
||||||
|
}; // ArrowData 생성을 끝낼거에요 -> 반환 준비 완료
|
||||||
|
} // 코드 블록을 끝낼거에요 -> GetArrowData를
|
||||||
|
|
||||||
if (pickupCollider == null || physicsCollider == null)
|
private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> Awake 범위를
|
||||||
AutoDetectColliders();
|
rb = GetComponent<Rigidbody>(); // 컴포넌트를 가져올거에요 -> Rigidbody를 찾아 rb에 저장
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
if (pickupCollider == null || physicsCollider == null) // 조건을 검사할거에요 -> 콜라이더 참조가 비어있는지
|
||||||
/// Collider 자동 탐지 (기존 로직 유지)
|
{ // 코드 블록을 시작할거에요 -> 자동 탐지 처리
|
||||||
/// </summary>
|
AutoDetectColliders(); // 함수를 실행할거에요 -> 콜라이더 자동 탐지
|
||||||
private void AutoDetectColliders()
|
} // 코드 블록을 끝낼거에요 -> 자동 탐지 처리
|
||||||
{
|
} // 코드 블록을 끝낼거에요 -> Awake를
|
||||||
Collider[] allColliders = GetComponents<Collider>();
|
|
||||||
|
|
||||||
foreach (Collider col in allColliders)
|
/// <summary> // 요약 주석을 시작할거에요 -> 콜라이더 자동 탐지 설명을
|
||||||
{
|
/// Collider 자동 탐지 (기존 로직 유지) // 설명할거에요 -> Sphere/Box를 찾아 자동 세팅
|
||||||
if (col is SphereCollider && pickupCollider == null)
|
/// </summary> // 요약 주석을 끝낼거에요 -> 설명 종료
|
||||||
{
|
private void AutoDetectColliders() // 함수를 선언할거에요 -> 콜라이더를 자동으로 찾는 AutoDetectColliders를
|
||||||
pickupCollider = col;
|
{ // 코드 블록을 시작할거에요 -> AutoDetectColliders 범위를
|
||||||
Debug.Log($"습득용 Collider: {col.GetType().Name}");
|
Collider[] allColliders = GetComponents<Collider>(); // 배열을 가져올거에요 -> 내 오브젝트의 모든 Collider를
|
||||||
}
|
|
||||||
else if (col is BoxCollider && physicsCollider == null)
|
|
||||||
{
|
|
||||||
physicsCollider = col;
|
|
||||||
Debug.Log($"물리용 Collider: {col.GetType().Name}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Start()
|
foreach (Collider col in allColliders) // 반복할거에요 -> 모든 콜라이더를 하나씩
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> 콜라이더 판별 범위
|
||||||
SetupAsItem();
|
if (col is SphereCollider && pickupCollider == null) // 조건을 검사할거에요 -> SphereCollider이고 pickupCollider가 비어있는지
|
||||||
}
|
{ // 코드 블록을 시작할거에요 -> 습득용 콜라이더 설정
|
||||||
|
pickupCollider = col; // 값을 넣을거에요 -> 습득용 콜라이더로 등록
|
||||||
|
Debug.Log($"습득용 Collider: {col.GetType().Name}"); // 로그를 찍을거에요 -> 어떤 타입이 잡혔는지
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 습득용 설정
|
||||||
|
else if (col is BoxCollider && physicsCollider == null) // 조건을 검사할거에요 -> BoxCollider이고 physicsCollider가 비어있는지
|
||||||
|
{ // 코드 블록을 시작할거에요 -> 물리용 콜라이더 설정
|
||||||
|
physicsCollider = col; // 값을 넣을거에요 -> 물리용 콜라이더로 등록
|
||||||
|
Debug.Log($"물리용 Collider: {col.GetType().Name}"); // 로그를 찍을거에요 -> 어떤 타입이 잡혔는지
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 물리용 설정
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 콜라이더 판별
|
||||||
|
} // 코드 블록을 끝낼거에요 -> AutoDetectColliders를
|
||||||
|
|
||||||
/// <summary>
|
private void Start() // 함수를 선언할거에요 -> 시작 시 1회 실행되는 Start를
|
||||||
/// 아이템 모드 설정 (기존 로직 유지 + 단순화)
|
{ // 코드 블록을 시작할거에요 -> Start 범위를
|
||||||
/// </summary>
|
SetupAsItem(); // 함수를 실행할거에요 -> 아이템 모드로 세팅
|
||||||
private void SetupAsItem()
|
} // 코드 블록을 끝낼거에요 -> Start를
|
||||||
{
|
|
||||||
GameObject player = GameObject.FindGameObjectWithTag("Player");
|
|
||||||
if (player != null)
|
|
||||||
{
|
|
||||||
playerTransform = player.transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pickupUI != null) pickupUI.SetActive(false);
|
/// <summary> // 요약 주석을 시작할거에요 -> 아이템 모드 세팅 설명을
|
||||||
|
/// 아이템 모드 설정 (기존 로직 유지 + 단순화) // 설명할거에요 -> UI/콜라이더/물리 설정을 정리
|
||||||
|
/// </summary> // 요약 주석을 끝낼거에요 -> 설명 종료
|
||||||
|
private void SetupAsItem() // 함수를 선언할거에요 -> 아이템 상태로 세팅하는 SetupAsItem을
|
||||||
|
{ // 코드 블록을 시작할거에요 -> SetupAsItem 범위를
|
||||||
|
GameObject player = GameObject.FindGameObjectWithTag("Player"); // 오브젝트를 찾을거에요 -> Player 태그 오브젝트를
|
||||||
|
if (player != null) // 조건을 검사할거에요 -> 플레이어를 찾았는지
|
||||||
|
{ // 코드 블록을 시작할거에요 -> 플레이어 참조 저장
|
||||||
|
playerTransform = player.transform; // 값을 넣을거에요 -> 플레이어 위치를 저장
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 플레이어 참조 저장
|
||||||
|
|
||||||
// Collider 설정
|
if (pickupUI != null) pickupUI.SetActive(false); // 조건이 맞으면 실행할거에요 -> 시작 시 UI를 숨기기
|
||||||
if (pickupCollider != null)
|
|
||||||
{
|
|
||||||
pickupCollider.enabled = true;
|
|
||||||
pickupCollider.isTrigger = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (physicsCollider != null)
|
// Collider 설정 // 설명을 적을거에요 -> 습득용/물리용 콜라이더 모드 설정
|
||||||
{
|
if (pickupCollider != null) // 조건을 검사할거에요 -> 습득용 콜라이더가 있는지
|
||||||
physicsCollider.enabled = true;
|
{ // 코드 블록을 시작할거에요 -> 습득용 콜라이더 설정
|
||||||
physicsCollider.isTrigger = false;
|
pickupCollider.enabled = true; // 값을 바꿀거에요 -> 습득용 콜라이더 활성화
|
||||||
}
|
pickupCollider.isTrigger = true; // 값을 바꿀거에요 -> 습득용은 트리거로
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 습득용 설정
|
||||||
|
|
||||||
// 물리 설정
|
if (physicsCollider != null) // 조건을 검사할거에요 -> 물리용 콜라이더가 있는지
|
||||||
if (rb != null)
|
{ // 코드 블록을 시작할거에요 -> 물리용 콜라이더 설정
|
||||||
{
|
physicsCollider.enabled = true; // 값을 바꿀거에요 -> 물리 콜라이더 활성화
|
||||||
rb.useGravity = true;
|
physicsCollider.isTrigger = false; // 값을 바꿀거에요 -> 물리용은 트리거 아님
|
||||||
rb.isKinematic = false;
|
} // 코드 블록을 끝낼거에요 -> 물리용 설정
|
||||||
rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Update()
|
// 물리 설정 // 설명을 적을거에요 -> 리지드바디 세팅
|
||||||
{
|
if (rb != null) // 조건을 검사할거에요 -> Rigidbody가 있는지
|
||||||
if (playerTransform == null || pickupUI == null) return;
|
{ // 코드 블록을 시작할거에요 -> Rigidbody 설정
|
||||||
|
rb.useGravity = true; // 값을 바꿀거에요 -> 중력 적용
|
||||||
|
rb.isKinematic = false; // 값을 바꿀거에요 -> 물리 시뮬레이션 적용
|
||||||
|
rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; // 값을 바꿀거에요 -> 빠른 충돌도 놓치지 않게
|
||||||
|
} // 코드 블록을 끝낼거에요 -> Rigidbody 설정
|
||||||
|
} // 코드 블록을 끝낼거에요 -> SetupAsItem을
|
||||||
|
|
||||||
float distance = Vector3.Distance(transform.position, playerTransform.position);
|
private void Update() // 함수를 선언할거에요 -> 매 프레임 실행되는 Update를
|
||||||
pickupUI.SetActive(distance <= uiDisplayRange);
|
{ // 코드 블록을 시작할거에요 -> Update 범위를
|
||||||
}
|
if (playerTransform == null || pickupUI == null) return; // 조건이 맞으면 종료할거에요 -> 플레이어/UI가 없으면 처리 불가
|
||||||
|
|
||||||
/// <summary>
|
float distance = Vector3.Distance(transform.position, playerTransform.position); // 값을 계산할거에요 -> 플레이어와의 거리를
|
||||||
/// [MODIFIED] PlayerInteraction에서 'E' 키를 눌렀을 때 호출
|
pickupUI.SetActive(distance <= uiDisplayRange); // 값을 적용할거에요 -> 거리 안이면 UI 켜고 밖이면 끄기
|
||||||
/// 기존: SwapArrow(gameObject) → 변경: SetCurrentArrow(ArrowData)
|
} // 코드 블록을 끝낼거에요 -> Update를
|
||||||
/// </summary>
|
|
||||||
public void Pickup(PlayerAttack playerAttack)
|
|
||||||
{
|
|
||||||
if (playerAttack == null) return;
|
|
||||||
|
|
||||||
// ArrowData를 패키징하여 PlayerAttack에 전달
|
/// <summary> // 요약 주석을 시작할거에요 -> 습득 함수 설명을
|
||||||
ArrowData data = GetArrowData();
|
/// [MODIFIED] PlayerInteraction에서 'E' 키를 눌렀을 때 호출 // 설명할거에요 -> 상호작용으로 호출되는 함수
|
||||||
playerAttack.SetCurrentArrow(data);
|
/// 기존: SwapArrow(gameObject) → 변경: SetCurrentArrow(ArrowData) // 설명할거에요 -> 전달 방식 변경점
|
||||||
|
/// </summary> // 요약 주석을 끝낼거에요 -> 설명 종료
|
||||||
|
public void Pickup(PlayerAttack playerAttack) // 함수를 선언할거에요 -> 플레이어가 화살을 줍는 Pickup을
|
||||||
|
{ // 코드 블록을 시작할거에요 -> Pickup 범위를
|
||||||
|
if (playerAttack == null) return; // 조건이 맞으면 종료할거에요 -> PlayerAttack이 없으면 진행 불가
|
||||||
|
|
||||||
Debug.Log($"[{arrowName}] 화살 습득! 속성: {elementType}");
|
ArrowData data = GetArrowData(); // 값을 만들거에요 -> 현재 화살 정보를 ArrowData로 패키징
|
||||||
|
playerAttack.SetCurrentArrow(data); // 함수를 실행할거에요 -> 플레이어 공격 스크립트에 현재 화살로 세팅
|
||||||
|
|
||||||
// 아이템 제거
|
Debug.Log($"[{arrowName}] 화살 습득! 속성: {elementType}"); // 로그를 찍을거에요 -> 습득/속성 확인
|
||||||
Destroy(gameObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
Destroy(gameObject); // 오브젝트를 삭제할거에요 -> 습득했으니 아이템 제거
|
||||||
/// 몬스터 드롭 시 화살 정보 설정 (기존 함수 유지)
|
} // 코드 블록을 끝낼거에요 -> Pickup을
|
||||||
/// </summary>
|
|
||||||
public void SetArrowData(string name, float dmg, float spd, float rng)
|
|
||||||
{
|
|
||||||
arrowName = name;
|
|
||||||
baseDamage = dmg;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary> // 요약 주석을 시작할거에요 -> 드롭 세팅 함수 설명을
|
||||||
/// [NEW] 속성 정보까지 포함한 전체 세팅 함수
|
/// 몬스터 드롭 시 화살 정보 설정 (기존 함수 유지) // 설명할거에요 -> 예전 호환용 함수
|
||||||
/// </summary>
|
/// </summary> // 요약 주석을 끝낼거에요 -> 설명 종료
|
||||||
public void SetArrowDataFull(
|
public void SetArrowData(string name, float dmg, float spd, float rng) // 함수를 선언할거에요 -> 간단 세팅용 SetArrowData를
|
||||||
string name,
|
{ // 코드 블록을 시작할거에요 -> SetArrowData 범위를
|
||||||
ArrowElementType element,
|
arrowName = name; // 값을 넣을거에요 -> 이름을 세팅
|
||||||
float baseDmg,
|
baseDamage = dmg; // 값을 넣을거에요 -> 기본 데미지를 세팅
|
||||||
float elemDmg,
|
} // 코드 블록을 끝낼거에요 -> SetArrowData를
|
||||||
float elemDur,
|
|
||||||
GameObject particlePrefab)
|
|
||||||
{
|
|
||||||
arrowName = name;
|
|
||||||
elementType = element;
|
|
||||||
baseDamage = baseDmg;
|
|
||||||
elementDamage = elemDmg;
|
|
||||||
elementDuration = elemDur;
|
|
||||||
projectilePrefab = particlePrefab;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary> // 요약 주석을 시작할거에요 -> 전체 세팅 함수 설명을
|
||||||
/// 바닥 충돌 시 속도 감쇠 (기존 로직 유지)
|
/// [NEW] 속성 정보까지 포함한 전체 세팅 함수 // 설명할거에요 -> 속성/파티클까지 세팅하는 버전
|
||||||
/// </summary>
|
/// </summary> // 요약 주석을 끝낼거에요 -> 설명 종료
|
||||||
private void OnCollisionEnter(Collision collision)
|
public void SetArrowDataFull( // 함수를 선언할거에요 -> 화살 모든 정보를 세팅하는 SetArrowDataFull을
|
||||||
{
|
string name, // 매개변수를 받을거에요 -> 화살 이름을
|
||||||
if (rb != null && collision.gameObject.CompareTag("Ground"))
|
ArrowElementType element, // 매개변수를 받을거에요 -> 속성 타입을
|
||||||
{
|
float baseDmg, // 매개변수를 받을거에요 -> 기본 데미지를
|
||||||
rb.velocity *= 0.5f;
|
float elemDmg, // 매개변수를 받을거에요 -> 속성 데미지를
|
||||||
rb.angularVelocity *= 0.5f;
|
float elemDur, // 매개변수를 받을거에요 -> 속성 지속 시간을
|
||||||
}
|
GameObject particlePrefab) // 매개변수를 받을거에요 -> 파티클 프리팹을
|
||||||
}
|
{ // 코드 블록을 시작할거에요 -> SetArrowDataFull 범위를
|
||||||
|
arrowName = name; // 값을 넣을거에요 -> 이름 세팅
|
||||||
|
elementType = element; // 값을 넣을거에요 -> 속성 세팅
|
||||||
|
baseDamage = baseDmg; // 값을 넣을거에요 -> 기본 데미지 세팅
|
||||||
|
elementDamage = elemDmg; // 값을 넣을거에요 -> 속성 데미지 세팅
|
||||||
|
elementDuration = elemDur; // 값을 넣을거에요 -> 속성 지속시간 세팅
|
||||||
|
projectilePrefab = particlePrefab; // 값을 넣을거에요 -> 파티클 프리팹 세팅
|
||||||
|
} // 코드 블록을 끝낼거에요 -> SetArrowDataFull을
|
||||||
|
|
||||||
#region 디버그 시각화
|
/// <summary> // 요약 주석을 시작할거에요 -> 바닥 충돌 감쇠 설명을
|
||||||
|
/// 바닥 충돌 시 속도 감쇠 (기존 로직 유지) // 설명할거에요 -> 바닥에 튕김을 줄이기
|
||||||
|
/// </summary> // 요약 주석을 끝낼거에요 -> 설명 종료
|
||||||
|
private void OnCollisionEnter(Collision collision) // 함수를 선언할거에요 -> 충돌이 발생하면 호출되는 OnCollisionEnter를
|
||||||
|
{ // 코드 블록을 시작할거에요 -> OnCollisionEnter 범위를
|
||||||
|
if (rb != null && collision.gameObject.CompareTag("Ground")) // 조건을 검사할거에요 -> Rigidbody가 있고 땅에 닿았는지
|
||||||
|
{ // 코드 블록을 시작할거에요 -> 감쇠 처리
|
||||||
|
rb.velocity *= 0.5f; // 값을 줄일거에요 -> 선속도를 절반으로
|
||||||
|
rb.angularVelocity *= 0.5f; // 값을 줄일거에요 -> 회전속도를 절반으로
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 감쇠 처리
|
||||||
|
} // 코드 블록을 끝낼거에요 -> OnCollisionEnter를
|
||||||
|
|
||||||
private void OnDrawGizmosSelected()
|
#region 디버그 시각화 // 영역을 표시할거에요 -> Gizmos 관련 코드 묶음임을
|
||||||
{
|
|
||||||
if (pickupCollider != null && pickupCollider.enabled)
|
|
||||||
{
|
|
||||||
Gizmos.color = Color.green;
|
|
||||||
if (pickupCollider is SphereCollider sphere)
|
|
||||||
{
|
|
||||||
Gizmos.DrawWireSphere(transform.position + sphere.center, sphere.radius);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (physicsCollider != null && physicsCollider.enabled)
|
private void OnDrawGizmosSelected() // 함수를 선언할거에요 -> 선택됐을 때 Gizmo를 그리는 OnDrawGizmosSelected를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> OnDrawGizmosSelected 범위를
|
||||||
Gizmos.color = Color.blue;
|
if (pickupCollider != null && pickupCollider.enabled) // 조건을 검사할거에요 -> 습득용 콜라이더가 있고 켜져있는지
|
||||||
if (physicsCollider is BoxCollider box)
|
{ // 코드 블록을 시작할거에요 -> 습득 범위 시각화
|
||||||
{
|
Gizmos.color = Color.green; // 색을 정할거에요 -> 초록색으로
|
||||||
Gizmos.matrix = transform.localToWorldMatrix;
|
if (pickupCollider is SphereCollider sphere) // 조건을 검사할거에요 -> 습득용이 SphereCollider인지
|
||||||
Gizmos.DrawWireCube(box.center, box.size);
|
{ // 코드 블록을 시작할거에요 -> 구체 범위 그리기
|
||||||
}
|
Gizmos.DrawWireSphere(transform.position + sphere.center, sphere.radius); // 선 구체를 그릴거에요 -> 습득 범위를
|
||||||
}
|
} // 코드 블록을 끝낼거에요 -> 구체 그리기
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 습득 범위 시각화
|
||||||
|
|
||||||
// [NEW] 속성 색상 표시
|
if (physicsCollider != null && physicsCollider.enabled) // 조건을 검사할거에요 -> 물리 콜라이더가 있고 켜져있는지
|
||||||
switch (elementType)
|
{ // 코드 블록을 시작할거에요 -> 물리 범위 시각화
|
||||||
{
|
Gizmos.color = Color.blue; // 색을 정할거에요 -> 파란색으로
|
||||||
case ArrowElementType.Fire: Gizmos.color = Color.red; break;
|
if (physicsCollider is BoxCollider box) // 조건을 검사할거에요 -> 물리용이 BoxCollider인지
|
||||||
case ArrowElementType.Ice: Gizmos.color = Color.cyan; break;
|
{ // 코드 블록을 시작할거에요 -> 박스 범위 그리기
|
||||||
case ArrowElementType.Poison: Gizmos.color = new Color(0.5f, 1f, 0f); break;
|
Gizmos.matrix = transform.localToWorldMatrix; // 좌표계를 바꿀거에요 -> 오브젝트 로컬 기준으로
|
||||||
case ArrowElementType.Lightning: Gizmos.color = Color.yellow; break;
|
Gizmos.DrawWireCube(box.center, box.size); // 선 박스를 그릴거에요 -> 물리 충돌 범위를
|
||||||
default: Gizmos.color = Color.white; break;
|
} // 코드 블록을 끝낼거에요 -> 박스 그리기
|
||||||
}
|
} // 코드 블록을 끝낼거에요 -> 물리 범위 시각화
|
||||||
Gizmos.DrawWireSphere(transform.position, 0.3f);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
// [NEW] 속성 색상 표시 // 설명을 적을거에요 -> 속성 타입에 따라 색을 다르게
|
||||||
}
|
switch (elementType) // 분기할거에요 -> elementType에 따라
|
||||||
|
{ // 코드 블록을 시작할거에요 -> switch 범위를
|
||||||
|
case ArrowElementType.Fire: Gizmos.color = Color.red; break; // 빨강으로 칠할거에요 -> 불 속성이면
|
||||||
|
case ArrowElementType.Ice: Gizmos.color = Color.cyan; break; // 하늘색으로 칠할거에요 -> 얼음 속성이면
|
||||||
|
case ArrowElementType.Poison: Gizmos.color = new Color(0.5f, 1f, 0f); break; // 연두로 칠할거에요 -> 독 속성이면
|
||||||
|
case ArrowElementType.Lightning: Gizmos.color = Color.yellow; break; // 노랑으로 칠할거에요 -> 번개 속성이면
|
||||||
|
default: Gizmos.color = Color.white; break; // 흰색으로 칠할거에요 -> 기본/없음이면
|
||||||
|
} // 코드 블록을 끝낼거에요 -> switch를
|
||||||
|
|
||||||
|
Gizmos.DrawWireSphere(transform.position, 0.3f); // 선 구체를 그릴거에요 -> 아이템 위치에 속성 표시용으로
|
||||||
|
} // 코드 블록을 끝낼거에요 -> OnDrawGizmosSelected를
|
||||||
|
|
||||||
|
#endregion // 영역을 끝낼거에요 -> Gizmos 묶음을
|
||||||
|
|
||||||
|
} // 코드 블록을 끝낼거에요 -> ArrowPickup을
|
||||||
|
|
@ -1,43 +1,42 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 플레이어의 전투 행동을 슬라이딩 윈도우(최근 N초) 기반으로 추적합니다.
|
/// 플레이어의 전투 행동을 슬라이딩 윈도우(최근 N초) 기반으로 추적합니다.
|
||||||
/// 보스 AI가 이 데이터를 읽어 카운터 패턴 발동 여부를 판단합니다.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PlayerBehaviorTracker : MonoBehaviour
|
public class PlayerBehaviorTracker : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerBehaviorTracker를
|
||||||
{
|
{
|
||||||
public static PlayerBehaviorTracker Instance { get; private set; }
|
public static PlayerBehaviorTracker Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스 접근용 Instance를
|
||||||
|
|
||||||
[Header("슬라이딩 윈도우 설정")]
|
[Header("슬라이딩 윈도우 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 슬라이딩 윈도우 설정 을
|
||||||
[Tooltip("행동 추적 윈도우 크기(초)")]
|
[Tooltip("행동 추적 윈도우 크기(초)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[SerializeField] private float windowDuration = 10f;
|
[SerializeField] private float windowDuration = 10f; // 변수를 선언할거에요 -> 추적 시간 범위(10초)를 windowDuration에
|
||||||
|
|
||||||
// ── 내부 기록용 타임스탬프 리스트 ──
|
// ── 내부 기록용 타임스탬프 리스트 ──
|
||||||
private List<float> dodgeTimestamps = new List<float>();
|
private List<float> dodgeTimestamps = new List<float>(); // 리스트를 만들거에요 -> 회피 시간들을 저장할 dodgeTimestamps를
|
||||||
private List<float> aimStartTimes = new List<float>();
|
private List<float> aimStartTimes = new List<float>(); // 리스트를 만들거에요 -> 조준 시작 시간들을 저장할 aimStartTimes를
|
||||||
private List<float> aimEndTimes = new List<float>();
|
private List<float> aimEndTimes = new List<float>(); // 리스트를 만들거에요 -> 조준 종료 시간들을 저장할 aimEndTimes를
|
||||||
private List<float> pierceShotTimestamps = new List<float>();
|
private List<float> pierceShotTimestamps = new List<float>(); // 리스트를 만들거에요 -> 관통샷 시간들을 저장할 pierceShotTimestamps를
|
||||||
private List<float> totalShotTimestamps = new List<float>();
|
private List<float> totalShotTimestamps = new List<float>(); // 리스트를 만들거에요 -> 전체 사격 시간들을 저장할 totalShotTimestamps를
|
||||||
|
|
||||||
// ── 현재 조준 상태 ──
|
// ── 현재 조준 상태 ──
|
||||||
private bool isAiming = false;
|
private bool isAiming = false; // 변수를 초기화할거에요 -> 조준 중 여부를 거짓(false)으로
|
||||||
private float currentAimStartTime;
|
private float currentAimStartTime; // 변수를 선언할거에요 -> 현재 조준 시작 시간을 저장할 currentAimStartTime을
|
||||||
|
|
||||||
// ── 외부에서 읽는 프로퍼티 ──
|
// ── 외부에서 읽는 프로퍼티 ──
|
||||||
public int DodgeCount => CountInWindow(dodgeTimestamps);
|
public int DodgeCount => CountInWindow(dodgeTimestamps); // 값을 반환할거에요 -> 최근 회피 횟수를 계산해서
|
||||||
public float AimHoldTime => CalculateAimHoldTime();
|
public float AimHoldTime => CalculateAimHoldTime(); // 값을 반환할거에요 -> 최근 조준 유지 시간을 계산해서
|
||||||
public float PierceRatio => CalculatePierceRatio();
|
public float PierceRatio => CalculatePierceRatio(); // 값을 반환할거에요 -> 최근 관통샷 비율을 계산해서
|
||||||
public int TotalShotsInWindow => CountInWindow(totalShotTimestamps);
|
public int TotalShotsInWindow => CountInWindow(totalShotTimestamps); // 값을 반환할거에요 -> 최근 총 발사 횟수를 계산해서
|
||||||
|
|
||||||
private void Awake()
|
private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 호출되는 Awake를
|
||||||
{
|
{
|
||||||
if (Instance != null && Instance != this)
|
if (Instance != null && Instance != this) // 조건이 맞으면 실행할거에요 -> 이미 다른 인스턴스가 존재한다면
|
||||||
{
|
{
|
||||||
Destroy(gameObject);
|
Destroy(gameObject); // 파괴할거에요 -> 중복된 이 오브젝트를
|
||||||
return;
|
return; // 중단할거에요 -> 초기화 로직을
|
||||||
}
|
}
|
||||||
Instance = this;
|
Instance = this; // 값을 저장할거에요 -> 내 자신(this)을 싱글톤 인스턴스에
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
@ -45,39 +44,39 @@ public class PlayerBehaviorTracker : MonoBehaviour
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
/// <summary>플레이어가 회피(대시/구르기)를 수행했을 때 호출</summary>
|
/// <summary>플레이어가 회피(대시/구르기)를 수행했을 때 호출</summary>
|
||||||
public void RecordDodge()
|
public void RecordDodge() // 함수를 선언할거에요 -> 회피를 기록하는 RecordDodge를
|
||||||
{
|
{
|
||||||
dodgeTimestamps.Add(Time.time);
|
dodgeTimestamps.Add(Time.time); // 추가할거에요 -> 현재 시간을 회피 목록에
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>플레이어가 조준을 시작했을 때 호출</summary>
|
/// <summary>플레이어가 조준을 시작했을 때 호출</summary>
|
||||||
public void RecordAimStart()
|
public void RecordAimStart() // 함수를 선언할거에요 -> 조준 시작을 기록하는 RecordAimStart를
|
||||||
{
|
{
|
||||||
if (!isAiming)
|
if (!isAiming) // 조건이 맞으면 실행할거에요 -> 이미 조준 중이 아니라면
|
||||||
{
|
{
|
||||||
isAiming = true;
|
isAiming = true; // 상태를 바꿀거에요 -> 조준 중 상태를 참(true)으로
|
||||||
currentAimStartTime = Time.time;
|
currentAimStartTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 조준 시작 시간으로
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>플레이어가 조준을 해제했을 때 호출</summary>
|
/// <summary>플레이어가 조준을 해제했을 때 호출</summary>
|
||||||
public void RecordAimEnd()
|
public void RecordAimEnd() // 함수를 선언할거에요 -> 조준 종료를 기록하는 RecordAimEnd를
|
||||||
{
|
{
|
||||||
if (isAiming)
|
if (isAiming) // 조건이 맞으면 실행할거에요 -> 조준 중이었다면
|
||||||
{
|
{
|
||||||
isAiming = false;
|
isAiming = false; // 상태를 바꿀거에요 -> 조준 중 상태를 거짓(false)으로
|
||||||
aimStartTimes.Add(currentAimStartTime);
|
aimStartTimes.Add(currentAimStartTime); // 추가할거에요 -> 시작 시간을 리스트에
|
||||||
aimEndTimes.Add(Time.time);
|
aimEndTimes.Add(Time.time); // 추가할거에요 -> 종료(현재) 시간을 리스트에
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>화살 발사 시 호출. isPierce=true면 관통 화살</summary>
|
/// <summary>화살 발사 시 호출. isPierce=true면 관통 화살</summary>
|
||||||
public void RecordShot(bool isPierce)
|
public void RecordShot(bool isPierce) // 함수를 선언할거에요 -> 발사를 기록하는 RecordShot을
|
||||||
{
|
{
|
||||||
totalShotTimestamps.Add(Time.time);
|
totalShotTimestamps.Add(Time.time); // 추가할거에요 -> 현재 시간을 전체 사격 목록에
|
||||||
if (isPierce)
|
if (isPierce) // 조건이 맞으면 실행할거에요 -> 관통 화살이라면
|
||||||
{
|
{
|
||||||
pierceShotTimestamps.Add(Time.time);
|
pierceShotTimestamps.Add(Time.time); // 추가할거에요 -> 현재 시간을 관통 사격 목록에
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,62 +85,61 @@ public class PlayerBehaviorTracker : MonoBehaviour
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
/// <summary>새 런 시작 시 모든 행동 기록을 초기화</summary>
|
/// <summary>새 런 시작 시 모든 행동 기록을 초기화</summary>
|
||||||
public void ResetForNewRun()
|
public void ResetForNewRun() // 함수를 선언할거에요 -> 모든 기록을 초기화하는 ResetForNewRun을
|
||||||
{
|
{
|
||||||
dodgeTimestamps.Clear();
|
dodgeTimestamps.Clear(); // 비울거에요 -> 회피 기록을
|
||||||
aimStartTimes.Clear();
|
aimStartTimes.Clear(); // 비울거에요 -> 조준 시작 기록을
|
||||||
aimEndTimes.Clear();
|
aimEndTimes.Clear(); // 비울거에요 -> 조준 종료 기록을
|
||||||
pierceShotTimestamps.Clear();
|
pierceShotTimestamps.Clear(); // 비울거에요 -> 관통 사격 기록을
|
||||||
totalShotTimestamps.Clear();
|
totalShotTimestamps.Clear(); // 비울거에요 -> 전체 사격 기록을
|
||||||
isAiming = false;
|
isAiming = false; // 초기화할거에요 -> 조준 상태를 거짓(false)으로
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
// 내부 계산
|
// 내부 계산
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
private int CountInWindow(List<float> timestamps)
|
private int CountInWindow(List<float> timestamps) // 함수를 선언할거에요 -> 윈도우 내 개수를 세는 CountInWindow를
|
||||||
{
|
{
|
||||||
float cutoff = Time.time - windowDuration;
|
float cutoff = Time.time - windowDuration; // 값을 계산할거에요 -> 현재 시간에서 윈도우 크기를 뺀 기준 시간을
|
||||||
// 오래된 항목 제거
|
timestamps.RemoveAll(t => t < cutoff); // 삭제할거에요 -> 기준 시간보다 오래된 기록들을 리스트에서
|
||||||
timestamps.RemoveAll(t => t < cutoff);
|
return timestamps.Count; // 반환할거에요 -> 남은 기록의 개수를
|
||||||
return timestamps.Count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private float CalculateAimHoldTime()
|
private float CalculateAimHoldTime() // 함수를 선언할거에요 -> 총 조준 시간을 계산하는 CalculateAimHoldTime을
|
||||||
{
|
{
|
||||||
float cutoff = Time.time - windowDuration;
|
float cutoff = Time.time - windowDuration; // 값을 계산할거에요 -> 기준 시간(현재 - 윈도우)을
|
||||||
float total = 0f;
|
float total = 0f; // 변수를 초기화할거에요 -> 총 시간을 0으로
|
||||||
|
|
||||||
// 완료된 조준 세션
|
// 완료된 조준 세션
|
||||||
for (int i = aimStartTimes.Count - 1; i >= 0; i--)
|
for (int i = aimStartTimes.Count - 1; i >= 0; i--) // 반복할거에요 -> 저장된 조준 기록들을 역순으로
|
||||||
{
|
{
|
||||||
if (aimEndTimes[i] < cutoff)
|
if (aimEndTimes[i] < cutoff) // 조건이 맞으면 실행할거에요 -> 종료 시간이 기준보다 옛날이라면
|
||||||
{
|
{
|
||||||
aimStartTimes.RemoveAt(i);
|
aimStartTimes.RemoveAt(i); // 삭제할거에요 -> 해당 시작 기록을
|
||||||
aimEndTimes.RemoveAt(i);
|
aimEndTimes.RemoveAt(i); // 삭제할거에요 -> 해당 종료 기록을
|
||||||
continue;
|
continue; // 건너뛸거에요 -> 다음 반복으로
|
||||||
}
|
}
|
||||||
float start = Mathf.Max(aimStartTimes[i], cutoff);
|
float start = Mathf.Max(aimStartTimes[i], cutoff); // 값을 계산할거에요 -> 시작 시간과 기준 시간 중 더 최근 것을
|
||||||
total += aimEndTimes[i] - start;
|
total += aimEndTimes[i] - start; // 값을 더할거에요 -> 유효한 조준 기간을 총 시간에
|
||||||
}
|
}
|
||||||
|
|
||||||
// 현재 조준 중인 시간도 포함
|
// 현재 조준 중인 시간도 포함
|
||||||
if (isAiming)
|
if (isAiming) // 조건이 맞으면 실행할거에요 -> 현재 조준 중이라면
|
||||||
{
|
{
|
||||||
float start = Mathf.Max(currentAimStartTime, cutoff);
|
float start = Mathf.Max(currentAimStartTime, cutoff); // 값을 계산할거에요 -> 조준 시작과 기준 중 최근 것을
|
||||||
total += Time.time - start;
|
total += Time.time - start; // 값을 더할거에요 -> 지금까지의 조준 시간을 총 시간에
|
||||||
}
|
}
|
||||||
|
|
||||||
return total;
|
return total; // 반환할거에요 -> 계산된 총 조준 시간을
|
||||||
}
|
}
|
||||||
|
|
||||||
private float CalculatePierceRatio()
|
private float CalculatePierceRatio() // 함수를 선언할거에요 -> 관통 비율을 계산하는 CalculatePierceRatio를
|
||||||
{
|
{
|
||||||
int totalShots = CountInWindow(totalShotTimestamps);
|
int totalShots = CountInWindow(totalShotTimestamps); // 값을 가져올거에요 -> 최근 총 발사 횟수를
|
||||||
if (totalShots == 0) return 0f;
|
if (totalShots == 0) return 0f; // 조건이 맞으면 반환할거에요 -> 발사 기록이 없으면 0을
|
||||||
|
|
||||||
int pierceShots = CountInWindow(pierceShotTimestamps);
|
int pierceShots = CountInWindow(pierceShotTimestamps); // 값을 가져올거에요 -> 최근 관통 발사 횟수를
|
||||||
return (float)pierceShots / totalShots;
|
return (float)pierceShots / totalShots; // 반환할거에요 -> 관통 횟수 나누기 전체 횟수(비율)를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,67 +1,67 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
public class PlayerInput : MonoBehaviour
|
public class PlayerInput : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerInput을
|
||||||
{
|
{
|
||||||
// 다른 스크립트에게 명령을 내려야 하니, 미리 참조 시킴
|
// 다른 스크립트에게 명령을 내려야 하니, 미리 참조 시킴
|
||||||
[SerializeField] private PlayerHealth health; // 죽었는지 확인용
|
[SerializeField] private PlayerHealth health; // 변수를 선언할거에요 -> 죽었는지 확인할 health를
|
||||||
[SerializeField] private PlayerMovement movement; // 이동 명령용
|
[SerializeField] private PlayerMovement movement; // 변수를 선언할거에요 -> 이동 명령을 내릴 movement를
|
||||||
[SerializeField] private PlayerAim aim; // 회전 명령용
|
[SerializeField] private PlayerAim aim; // 변수를 선언할거에요 -> 회전 명령을 내릴 aim을
|
||||||
[SerializeField] private PlayerInteraction interaction; // 아이템 줍기 명령용
|
[SerializeField] private PlayerInteraction interaction; // 변수를 선언할거에요 -> 상호작용 명령을 내릴 interaction을
|
||||||
[SerializeField] private PlayerAttack attack;
|
[SerializeField] private PlayerAttack attack; // 변수를 선언할거에요 -> 공격 명령을 내릴 attack을
|
||||||
[SerializeField] private PlayerStatsUI statsUI; // UI 창 열기 명령용
|
[SerializeField] private PlayerStatsUI statsUI; // 변수를 선언할거에요 -> UI 토글을 위한 statsUI를
|
||||||
|
|
||||||
private void Update()
|
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를
|
||||||
{
|
{
|
||||||
// 1. 사망 체크
|
// 1. 사망 체크
|
||||||
// 죽었는데 키보드 눌린다고 시체가 움직이면 안 되니까 아예 입력을 차단(return)함
|
// 죽었는데 키보드 눌린다고 시체가 움직이면 안 되니까 아예 입력을 차단(return)함
|
||||||
if (health != null && health.IsDead) return;
|
if (health != null && health.IsDead) return; // 조건이 맞으면 중단할거에요 -> 플레이어가 죽었다면
|
||||||
|
|
||||||
// 2. UI 토글 (C키)
|
// 2. UI 토글 (C키)
|
||||||
// 스탯 창을 껐다 켰다 함
|
// 스탯 창을 껐다 켰다 함
|
||||||
if (Input.GetKeyDown(KeyCode.C) && statsUI != null) statsUI.ToggleWindow();
|
if (Input.GetKeyDown(KeyCode.C) && statsUI != null) statsUI.ToggleWindow(); // 조건이 맞으면 실행할거에요 -> C키를 눌렀다면 스탯창 토글을
|
||||||
|
|
||||||
// 3. 이동 입력 감지
|
// 3. 이동 입력 감지
|
||||||
// GetAxisRaw를 쓴 이유: 0에서 1로 부드럽게 변하는 게 아니라,
|
// GetAxisRaw를 쓴 이유: 0에서 1로 부드럽게 변하는 게 아니라,
|
||||||
// 키를 누르면 즉시 1, 떼면 0이 되어서 빠릿빠릿한 조작감을 줌
|
// 키를 누르면 즉시 1, 떼면 0이 되어서 빠릿빠릿한 조작감을 줌
|
||||||
float h = Input.GetAxisRaw("Horizontal"); // A, D 키 (-1, 0, 1)
|
float h = Input.GetAxisRaw("Horizontal"); // 값을 가져올거에요 -> 좌우 입력값(-1, 0, 1)을 h에
|
||||||
float v = Input.GetAxisRaw("Vertical"); // W, S 키 (-1, 0, 1)
|
float v = Input.GetAxisRaw("Vertical"); // 값을 가져올거에요 -> 상하 입력값(-1, 0, 1)을 v에
|
||||||
bool sprint = Input.GetKey(KeyCode.LeftShift); // Shift 누르고 있나?
|
bool sprint = Input.GetKey(KeyCode.LeftShift); // 값을 가져올거에요 -> 쉬프트 키 입력 여부를 sprint에
|
||||||
|
|
||||||
// 4. 이동 명령 하달
|
// 4. 이동 명령 하달
|
||||||
if (movement != null)
|
if (movement != null) // 조건이 맞으면 실행할거에요 -> 이동 스크립트가 있다면
|
||||||
{
|
{
|
||||||
// 방향 벡터를 정규화(.normalized)해서 대각선 이동 시 빨라지는 걸 방지
|
// 방향 벡터를 정규화(.normalized)해서 대각선 이동 시 빨라지는 걸 방지
|
||||||
// 그리고 달리기 여부(sprint)도 같이 넘겨줍니다.
|
// 그리고 달리기 여부(sprint)도 같이 넘겨줍니다.
|
||||||
movement.SetMoveInput(new Vector3(h, 0, v).normalized, sprint);
|
movement.SetMoveInput(new Vector3(h, 0, v).normalized, sprint); // 실행할거에요 -> 이동 입력 전달 함수를
|
||||||
|
|
||||||
// ⭐ [추가] 스페이스바 누르면 대시 명령!
|
// ⭐ [추가] 스페이스바 누르면 대시 명령!
|
||||||
if (Input.GetKeyDown(KeyCode.Space)) movement.AttemptDash();
|
if (Input.GetKeyDown(KeyCode.Space)) movement.AttemptDash(); // 조건이 맞으면 실행할거에요 -> 스페이스바 누르면 대시 시도를
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 마우스 회전 명령
|
// 5. 마우스 회전 명령
|
||||||
// 캐릭터가 마우스 커서를 바라보게 합니다.
|
// 캐릭터가 마우스 커서를 바라보게 합니다.
|
||||||
if (aim != null) aim.RotateTowardsMouse();
|
if (aim != null) aim.RotateTowardsMouse(); // 조건이 맞으면 실행할거에요 -> 마우스 방향 회전 함수를
|
||||||
|
|
||||||
// 6. 상호작용 (F키)
|
// 6. 상호작용 (F키)
|
||||||
// 바닥에 떨어진 무기를 줍기
|
// 바닥에 떨어진 무기를 줍기
|
||||||
if (Input.GetKeyDown(KeyCode.F) && interaction != null) interaction.TryInteract();
|
if (Input.GetKeyDown(KeyCode.F) && interaction != null) interaction.TryInteract(); // 조건이 맞으면 실행할거에요 -> F키 누르면 상호작용 시도를
|
||||||
|
|
||||||
// 7. 공격 입력 (좌클릭/우클릭)
|
// 7. 공격 입력 (좌클릭/우클릭)
|
||||||
if (attack != null)
|
if (attack != null) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있다면
|
||||||
{
|
{
|
||||||
// 우클릭 꾹: 차징(모으기) 시작
|
// 우클릭 꾹: 차징(모으기) 시작
|
||||||
if (Input.GetMouseButtonDown(1)) attack.StartCharging();
|
if (Input.GetMouseButtonDown(1)) attack.StartCharging(); // 조건이 맞으면 실행할거에요 -> 우클릭 누르면 차징 시작을
|
||||||
|
|
||||||
// 우클릭 뗌: 차징 취소 (공격 안 하고 캔슬)
|
// 우클릭 뗌: 차징 취소 (공격 안 하고 캔슬)
|
||||||
if (Input.GetMouseButtonUp(1)) attack.CancelCharging();
|
if (Input.GetMouseButtonUp(1)) attack.CancelCharging(); // 조건이 맞으면 실행할거에요 -> 우클릭 떼면 차징 취소를
|
||||||
|
|
||||||
// 좌클릭: 공격 시도
|
// 좌클릭: 공격 시도
|
||||||
if (Input.GetMouseButtonDown(0))
|
if (Input.GetMouseButtonDown(0)) // 조건이 맞으면 실행할거에요 -> 좌클릭을 눌렀다면
|
||||||
{
|
{
|
||||||
// 만약 우클릭(방어/조준) 상태에서 좌클릭을 했다면? -> 투척(Release)
|
// 만약 우클릭(방어/조준) 상태에서 좌클릭을 했다면? -> 투척(Release)
|
||||||
if (Input.GetMouseButton(1)) attack.ReleaseAttack();
|
if (Input.GetMouseButton(1)) attack.ReleaseAttack(); // 조건이 맞으면 실행할거에요 -> 차징 중이라면 발사를
|
||||||
// 그냥 좌클릭만 했다면? -> 일반 평타(Normal Attack)
|
// 그냥 좌클릭만 했다면? -> 일반 평타(Normal Attack)
|
||||||
else attack.PerformNormalAttack();
|
else attack.PerformNormalAttack(); // 그 외엔 실행할거에요 -> 일반 공격을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,277 +1,216 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using System.Collections;
|
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
||||||
|
|
||||||
|
[RequireComponent(typeof(CharacterController))] // 컴포넌트를 강제로 추가할거에요 -> CharacterController를 (이 스크립트를 넣으면 자동으로)
|
||||||
[RequireComponent(typeof(CharacterController))] // 이 스크립트를 넣으면 CharacterController도 자동으로 추가됨 (실수 방지)
|
public class PlayerMovement : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerMovement를
|
||||||
public class PlayerMovement : MonoBehaviour
|
|
||||||
{
|
{
|
||||||
[Header("=== 참조 ===")]
|
[Header("=== 참조 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 참조 === 를
|
||||||
// 외부 스크립트들을 상속 시키기 위한 변수들
|
|
||||||
// 'private'에 [SerializeField]를 붙여 에디터 인스펙터 창에서 설정 가능
|
|
||||||
[SerializeField] private Stats stats; // 이동 속도(Stat)를 가져오기 위해 필요
|
|
||||||
[SerializeField] private PlayerHealth health; // 죽거나 맞았을 때 이동을 멈추기 위해 필요
|
|
||||||
[SerializeField] private PlayerAnimator pAnim; // 이동에 맞춰 뛰는 모션을 재생하기 위해 필요
|
|
||||||
[SerializeField] private PlayerAttack attackScript; // 공격 중일 때 이동을 막기 위해 필요
|
|
||||||
|
|
||||||
[Header("=== CharacterController ===")]
|
[SerializeField] private Stats stats; // 변수를 선언하고 인스펙터에 노출할거에요 -> 이동 속도 정보를 가진 Stats 스크립트를
|
||||||
//CharacterControlle는 유니티가 제공하는 인간형 이동 컴포넌트
|
[SerializeField] private PlayerHealth health; // 변수를 선언하고 인스펙터에 노출할거에요 -> 사망 및 피격 상태를 알기 위한 PlayerHealth를
|
||||||
// Rigidbody와 달리 경사면, 계단 처리가 자동으로 됨.
|
[SerializeField] private PlayerAnimator pAnim; // 변수를 선언하고 인스펙터에 노출할거에요 -> 애니메이션을 제어할 PlayerAnimator를
|
||||||
private CharacterController _controller;
|
[SerializeField] private PlayerAttack attackScript; // 변수를 선언하고 인스펙터에 노출할거에요 -> 공격 상태를 알기 위한 PlayerAttack을
|
||||||
|
|
||||||
[Header("=== 대시 설정 ===")]
|
[Header("=== CharacterController ===")] // 인스펙터 창에 제목을 표시할거에요 -> === CharacterController === 를
|
||||||
[SerializeField] private float dashDistance = 3f; // 대시로 이동할 총 거리
|
private CharacterController _controller; // 변수를 선언할거에요 -> 이동을 담당할 CharacterController 컴포넌트를 담을 _controller를
|
||||||
[SerializeField] private float dashDuration = 0.08f; // 대시가 끝나는 시간 (짧을수록 빠름)
|
|
||||||
[SerializeField] private float dashCooldown = 1.5f; // 대시 후 다시 쓰기까지 기다리는 시간
|
|
||||||
|
|
||||||
[Header("=== 차징 감속 설정 ===")]
|
[Header("=== 대시 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 대시 설정 === 을
|
||||||
[Range(0.1f, 1f)] // 슬라이더로 조절하게 만듦 (1이면 감속 없음, 0.1이면 엄청 느려짐)
|
[SerializeField] private float dashDistance = 3f; // 변수를 선언할거에요 -> 대시로 이동할 거리(3.0)를 dashDistance에
|
||||||
[SerializeField] private float minSpeedMultiplier = 0.3f; // 차징 중일 때 속도를 30%로 줄임
|
[SerializeField] private float dashDuration = 0.08f; // 변수를 선언할거에요 -> 대시가 지속될 시간(0.08초)을 dashDuration에
|
||||||
|
[SerializeField] private float dashCooldown = 1.5f; // 변수를 선언할거에요 -> 대시 재사용 대기시간(1.5초)을 dashCooldown에
|
||||||
|
|
||||||
[Header("=== 중력 설정 ===")]
|
[Header("=== 차징 감속 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 차징 감속 설정 === 을
|
||||||
[SerializeField] private float gravity = -20f; // 지구 중력(-9.81)보다 세게 해서 점프 후 빨리 떨어지게 함 (조작감 향상)
|
[Range(0.1f, 1f)] // 슬라이더를 만들거에요 -> 0.1부터 1 사이의 값으로 조절하는
|
||||||
|
[SerializeField] private float minSpeedMultiplier = 0.3f; // 변수를 선언할거에요 -> 차징 중 속도 배율(30%)을 minSpeedMultiplier에
|
||||||
|
|
||||||
[Header("=== 충돌 설정 ===")]
|
[Header("=== 중력 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 중력 설정 === 을
|
||||||
[Tooltip("무기 레이어 (이 레이어와는 충돌하지 않음)")]
|
[SerializeField] private float gravity = -20f; // 변수를 선언할거에요 -> 중력 가속도(-20)를 gravity에
|
||||||
[SerializeField] private LayerMask weaponLayer; // 무기를 밟고 올라가는 버그를 막기 위해 무기 레이어를 지정
|
|
||||||
|
|
||||||
//[Tooltip("플레이어가 서 있어야 하는 최소 높이 (버그 방지)")]
|
[Header("=== 충돌 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 충돌 설정 === 을
|
||||||
//[SerializeField] private float minGroundHeight = 0.1f; // 땅 보정 시 바닥에 파묻히지 않게 살짝 띄워주는 값
|
[Tooltip("무기 레이어 (이 레이어와는 충돌하지 않음)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
|
[SerializeField] private LayerMask weaponLayer; // 변수를 선언할거에요 -> 충돌을 무시할 무기 레이어를 weaponLayer에
|
||||||
|
|
||||||
// 내부 상태 변수 (로직 계산용)
|
// 내부 상태 변수 (로직 계산용)
|
||||||
private Vector3 _moveDir; // 키보드 입력(WASD)으로 결정된 이동 방향
|
private Vector3 _moveDir; // 변수를 선언할거에요 -> 입력받은 이동 방향을 저장할 _moveDir을
|
||||||
private bool _isSprinting; // Shift 키를 눌렀는지 여부
|
private bool _isSprinting; // 변수를 선언할거에요 -> 달리기 중인지 여부를 저장할 _isSprinting을
|
||||||
private bool _isDashing; // 지금 대시 중인가? (중복 대시 방지)
|
private bool _isDashing; // 변수를 선언할거에요 -> 현재 대시 중인지 여부를 저장할 _isDashing을
|
||||||
private float _lastDashTime; // 마지막으로 대시를 쓴 시간 (쿨타임 계산용)
|
private float _lastDashTime; // 변수를 선언할거에요 -> 마지막으로 대시를 쓴 시간을 저장할 _lastDashTime을
|
||||||
private float _verticalVelocity; // 수직 속도 (중력 가속도 계산용)
|
private float _verticalVelocity; // 변수를 선언할거에요 -> 수직(낙하) 속도를 저장할 _verticalVelocity를
|
||||||
|
|
||||||
// 디버그용 (초기 위치 저장)
|
// 디버그용 (초기 위치 저장)
|
||||||
private float _initialYPosition;
|
private float _initialYPosition; // 변수를 선언할거에요 -> 시작 높이를 저장할 _initialYPosition을
|
||||||
|
|
||||||
private void Awake()
|
private void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 가장 먼저 호출되는 Awake를
|
||||||
{
|
{
|
||||||
// 내 몸에 붙어있는 CharacterController를 가져옴
|
_controller = GetComponent<CharacterController>(); // 컴포넌트를 가져와서 저장할거에요 -> 내 몸에 있는 CharacterController를 _controller에
|
||||||
_controller = GetComponent<CharacterController>();
|
|
||||||
|
|
||||||
// 만약 없다면 콘솔창에 에러를 띄움
|
if (_controller == null) // 조건이 맞으면 실행할거에요 -> 컨트롤러를 찾지 못했다면
|
||||||
if (_controller == null)
|
|
||||||
{
|
{
|
||||||
Debug.LogError("[PlayerMovement] CharacterController가 필요합니다!");
|
Debug.LogError("[PlayerMovement] CharacterController가 필요합니다!"); // 에러 로그를 띄울거에요 -> 컴포넌트가 필요하다는 메시지를
|
||||||
}
|
}
|
||||||
|
|
||||||
_initialYPosition = transform.position.y;
|
_initialYPosition = transform.position.y; // 값을 저장할거에요 -> 현재의 Y축 위치를 _initialYPosition에
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Start()
|
private void Start() // 함수를 실행할거에요 -> 첫 프레임 시작 전에 호출되는 Start를
|
||||||
{
|
{
|
||||||
// 게임 시작 시 딱 한 번 실행: 무기와 플레이어끼리 충돌 끄기
|
SetupLayerCollision(); // 함수를 실행할거에요 -> 레이어 충돌 설정을 담당하는 SetupLayerCollision을
|
||||||
SetupLayerCollision();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// 레이어 충돌 무시 설정 (플레이어 자살 버그 방지)
|
/// 레이어 충돌 무시 설정 (플레이어 자살 버그 방지)
|
||||||
|
private void SetupLayerCollision() // 함수를 선언할거에요 -> 레이어 충돌을 설정하는 SetupLayerCollision을
|
||||||
private void SetupLayerCollision()
|
|
||||||
{
|
{
|
||||||
int playerLayer = gameObject.layer; // 내 레이어 번호 가져오기
|
int playerLayer = gameObject.layer; // 값을 가져올거에요 -> 내 게임 오브젝트의 레이어 번호를 playerLayer에
|
||||||
|
|
||||||
// 무기 레이어가 설정되어 있다면
|
if (weaponLayer != 0) // 조건이 맞으면 실행할거에요 -> 무기 레이어가 설정되어 있다면(0이 아니라면)
|
||||||
if (weaponLayer != 0)
|
|
||||||
{
|
{
|
||||||
// 비트 연산으로 되어있는 LayerMask에서 숫자 인덱스를 뽑음
|
int weaponLayerIndex = GetLayerFromMask(weaponLayer); // 함수를 실행해서 값을 받을거에요 -> 비트마스크를 정수 인덱스로 변환해서 weaponLayerIndex에
|
||||||
int weaponLayerIndex = GetLayerFromMask(weaponLayer);
|
|
||||||
|
|
||||||
// 유효한 레이어라면 물리 엔진에게 충돌하지 말라고 명령.
|
if (weaponLayerIndex >= 0) // 조건이 맞으면 실행할거에요 -> 유효한 레이어 인덱스(0 이상)라면
|
||||||
|
|
||||||
if (weaponLayerIndex >= 0)
|
|
||||||
{
|
{
|
||||||
Physics.IgnoreLayerCollision(playerLayer, weaponLayerIndex, true);
|
Physics.IgnoreLayerCollision(playerLayer, weaponLayerIndex, true); // 설정을 변경할거에요 -> 플레이어와 무기 레이어 간의 충돌을 무시하도록(true)
|
||||||
Debug.Log($"[PlayerMovement] {LayerMask.LayerToName(playerLayer)}와 {LayerMask.LayerToName(weaponLayerIndex)} 간 충돌 무시 설정 완료");
|
Debug.Log($"[PlayerMovement] {LayerMask.LayerToName(playerLayer)}와 {LayerMask.LayerToName(weaponLayerIndex)} 간 충돌 무시 설정 완료"); // 로그를 출력할거에요 -> 충돌 무시 설정이 완료되었다는 메시지를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 비트 마스크(이진수)를 정수 인덱스로 변환하는 수학 함수
|
// 비트 마스크(이진수)를 정수 인덱스로 변환하는 수학 함수
|
||||||
private int GetLayerFromMask(LayerMask mask)
|
private int GetLayerFromMask(LayerMask mask) // 함수를 선언할거에요 -> 마스크를 인덱스로 바꾸는 GetLayerFromMask를
|
||||||
{
|
{
|
||||||
int layerNumber = 0;
|
int layerNumber = 0; // 변수를 초기화할거에요 -> 레이어 번호를 셀 layerNumber를 0으로
|
||||||
int layer = mask.value;
|
int layer = mask.value; // 값을 가져올거에요 -> 마스크의 실제 정수값을 layer에
|
||||||
while (layer > 1)
|
while (layer > 1) // 반복할거에요 -> layer 값이 1보다 클 때까지
|
||||||
{
|
{
|
||||||
layer = layer >> 1; // 비트를 오른쪽으로 밀면서 횟수를 셉니다.
|
layer = layer >> 1; // 비트 연산을 할거에요 -> 비트를 오른쪽으로 한 칸 밀어서(나누기 2)
|
||||||
layerNumber++;
|
layerNumber++; // 값을 증가시킬거에요 -> 레이어 번호 카운트를 1만큼
|
||||||
}
|
}
|
||||||
return layerNumber;
|
return layerNumber; // 값을 반환할거에요 -> 계산된 레이어 번호를
|
||||||
}
|
}
|
||||||
|
|
||||||
// 외부(InputHandler)에서 키보드 입력을 넣어주는 함수
|
// 외부(InputHandler)에서 키보드 입력을 넣어주는 함수
|
||||||
public void SetMoveInput(Vector3 dir, bool sprint)
|
public void SetMoveInput(Vector3 dir, bool sprint) // 함수를 선언할거에요 -> 이동 입력을 받아오는 SetMoveInput을
|
||||||
{
|
{
|
||||||
_moveDir = dir; // 방향 저장
|
_moveDir = dir; // 값을 저장할거에요 -> 입력받은 방향(dir)을 _moveDir에
|
||||||
_isSprinting = sprint; // 달리기 여부 저장
|
_isSprinting = sprint; // 값을 저장할거에요 -> 달리기 여부(sprint)를 _isSprinting에
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AttemptDash()
|
public void AttemptDash() // 함수를 선언할거에요 -> 대시를 시도하는 AttemptDash를
|
||||||
{
|
{
|
||||||
// 쿨타임, 생존 여부 등을 체크하고 통과하면 대시 시작
|
if (CanDash()) StartCoroutine(DashRoutine()); // 조건이 맞으면 실행할거에요 -> 대시가 가능하다면(CanDash) 대시 코루틴(DashRoutine)을
|
||||||
if (CanDash()) StartCoroutine(DashRoutine());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanDash()
|
private bool CanDash() // 함수를 선언할거에요 -> 대시 가능 여부를 판단하는 CanDash를
|
||||||
{
|
{
|
||||||
// 현재 시간 > 마지막 대시 시간 + 쿨타임 (즉, 쿨타임 지남)
|
bool isCooldownOver = Time.time >= _lastDashTime + dashCooldown; // 조건을 검사할거에요 -> 현재 시간이 쿨타임 이후인지 확인해서 isCooldownOver에
|
||||||
bool isCooldownOver = Time.time >= _lastDashTime + dashCooldown;
|
bool isAlive = health != null && !health.IsDead; // 조건을 검사할거에요 -> 체력 스크립트가 있고 살아있는지 확인해서 isAlive에
|
||||||
// 체력 스크립트가 있다면 살아있는지 확인
|
return isCooldownOver && !_isDashing && isAlive; // 결과를 반환할거에요 -> 쿨타임 끝남, 대시 안 함, 살아있음이 모두 참일 때만 true를
|
||||||
bool isAlive = health != null && !health.IsDead;
|
|
||||||
// 쿨타임 끝남 && 대시 중 아님 && 살아있음 -> true
|
|
||||||
return isCooldownOver && !_isDashing && isAlive;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를
|
||||||
{
|
{
|
||||||
// 1. 행동 불가 상태 체크 (가장 먼저 해서 불필요한 연산 방지)
|
// 1. 행동 불가 상태 체크 (가장 먼저 해서 불필요한 연산 방지)
|
||||||
// 죽었거나, 맞았거나, 대시 중이면 키보드 이동을 막습니다.
|
if (health != null && (health.IsDead || health.isHit || _isDashing)) // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있고 (죽었거나, 맞았거나, 대시 중이라면)
|
||||||
if (health != null && (health.IsDead || health.isHit || _isDashing))
|
|
||||||
{
|
{
|
||||||
if (pAnim != null && !health.IsDead) pAnim.UpdateMove(0f); // 애니메이션 멈춤
|
if (pAnim != null && !health.IsDead) pAnim.UpdateMove(0f); // 조건이 맞으면 실행할거에요 -> 애니메이터가 있고 죽지 않았다면 이동 모션을 0(정지)으로
|
||||||
ApplyGravityOnly(); // 이동은 못 해도 중력은 받아야 바닥으로 떨어짐
|
ApplyGravityOnly(); // 함수를 실행할거에요 -> 이동은 안 해도 중력은 적용하는 ApplyGravityOnly를
|
||||||
return;
|
return; // 중단할거에요 -> 더 이상 움직임 코드를 실행하지 않도록 함수를
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 공격 중 이동 차단
|
// 2. 공격 중 이동 차단
|
||||||
// 공격 모션 중에 미끄러지듯 이동하는 '스케이트 현상' 방지
|
if (attackScript != null && attackScript.IsAttacking) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있고 공격 중이라면
|
||||||
if (attackScript != null && attackScript.IsAttacking)
|
|
||||||
{
|
{
|
||||||
if (pAnim != null) pAnim.UpdateMove(0f);
|
if (pAnim != null) pAnim.UpdateMove(0f); // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 이동 모션을 0(정지)으로
|
||||||
ApplyGravityOnly();
|
ApplyGravityOnly(); // 함수를 실행할거에요 -> 제자리에서 중력만 받는 ApplyGravityOnly를
|
||||||
return;
|
return; // 중단할거에요 -> 이동을 막기 위해 함수를
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 이동 속도 계산
|
// 3. 이동 속도 계산
|
||||||
// Shift 눌렀으면 달리기 속도, 아니면 걷기 속도
|
float speed = _isSprinting ? stats.CurrentRunSpeed : stats.CurrentMoveSpeed; // 값을 결정할거에요 -> 달리기 중이면 런 스피드를, 아니면 이동 스피드를 speed에
|
||||||
float speed = _isSprinting ? stats.CurrentRunSpeed : stats.CurrentMoveSpeed;
|
|
||||||
|
|
||||||
// 공격 차징 중이라면 속도를 느리게 만듦 (긴장감 조성)
|
// 공격 차징 중이라면 속도를 느리게 만듦 (긴장감 조성)
|
||||||
if (attackScript != null && attackScript.IsCharging)
|
if (attackScript != null && attackScript.IsCharging) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있고 차징 중이라면
|
||||||
{
|
{
|
||||||
// Lerp를 써서 차징 단계에 따라 부드럽게 감속
|
float speedReduction = Mathf.Lerp(1.0f, minSpeedMultiplier, attackScript.ChargeProgress); // 값을 계산할거에요 -> 차징 진행도에 따라 속도 배율을 줄여서 speedReduction에
|
||||||
float speedReduction = Mathf.Lerp(1.0f, minSpeedMultiplier, attackScript.ChargeProgress);
|
speed *= speedReduction; // 값을 곱할거에요 -> 현재 속도(speed)에 감속 배율을
|
||||||
speed *= speedReduction;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 최종 이동 벡터 계산
|
// 4. 최종 이동 벡터 계산
|
||||||
// 방향 * 속도 * 시간(프레임 보정) = 이번 프레임에 움직일 거리
|
Vector3 motion = _moveDir * speed * Time.deltaTime; // 벡터를 계산할거에요 -> 방향 * 속도 * 시간을 곱해서 이번 프레임 이동량을 motion에
|
||||||
Vector3 motion = _moveDir * speed * Time.deltaTime;
|
|
||||||
|
|
||||||
// 중력 계산 (Y축)
|
// 중력 계산 (Y축)
|
||||||
ApplyGravity();
|
ApplyGravity(); // 함수를 실행할거에요 -> 수직 속도를 계산하는 ApplyGravity를
|
||||||
motion.y = _verticalVelocity * Time.deltaTime;
|
motion.y = _verticalVelocity * Time.deltaTime; // 값을 넣을거에요 -> 수직 이동량(속도 * 시간)을 motion의 y값에
|
||||||
|
|
||||||
// ⭐ 실제 이동 실행 (여기서 벽 충돌 처리가 자동 수행됨)
|
// ⭐ 실제 이동 실행 (여기서 벽 충돌 처리가 자동 수행됨)
|
||||||
_controller.Move(motion);
|
_controller.Move(motion); // 이동시킬거에요 -> 캐릭터 컨트롤러를 motion 벡터만큼
|
||||||
|
|
||||||
// 5. 안전장치 가동 (승천 버그 체크)
|
|
||||||
//CheckAbnormalHeight();
|
|
||||||
|
|
||||||
// 6. 애니메이션 업데이트 (걷기/뛰기 모션)
|
// 6. 애니메이션 업데이트 (걷기/뛰기 모션)
|
||||||
UpdateAnimation();
|
UpdateAnimation(); // 함수를 실행할거에요 -> 이동 애니메이션을 갱신하는 UpdateAnimation을
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyGravity()
|
private void ApplyGravity() // 함수를 선언할거에요 -> 중력을 계산하는 ApplyGravity를
|
||||||
{
|
{
|
||||||
if (_controller.isGrounded)
|
if (_controller.isGrounded) // 조건이 맞으면 실행할거에요 -> 캐릭터가 땅에 닿아있다면
|
||||||
{
|
{
|
||||||
// 땅에 닿아있어도 -2f 정도로 계속 눌러줘야 경사면에서 붕 뜨지 않음
|
_verticalVelocity = -2f; // 값을 설정할거에요 -> 바닥에 딱 붙어있도록 약한 하향 속도(-2)를 _verticalVelocity에
|
||||||
_verticalVelocity = -2f;
|
|
||||||
}
|
}
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 공중에 떠 있다면
|
||||||
{
|
{
|
||||||
// 공중에 있다면 중력 가속도 누적 (점점 빨라짐)
|
_verticalVelocity += gravity * Time.deltaTime; // 값을 더할거에요 -> 중력 가속도를 시간에 맞춰 _verticalVelocity에
|
||||||
_verticalVelocity += gravity * Time.deltaTime;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 키보드 이동 없이 중력만 적용하는 함수 (피격/공격 중 사용)
|
// 키보드 이동 없이 중력만 적용하는 함수 (피격/공격 중 사용)
|
||||||
private void ApplyGravityOnly()
|
private void ApplyGravityOnly() // 함수를 선언할거에요 -> 중력만 적용해서 움직이는 ApplyGravityOnly를
|
||||||
{
|
{
|
||||||
ApplyGravity();
|
ApplyGravity(); // 함수를 실행할거에요 -> 수직 속도를 계산하는 ApplyGravity를
|
||||||
_controller.Move(new Vector3(0, _verticalVelocity * Time.deltaTime, 0));
|
_controller.Move(new Vector3(0, _verticalVelocity * Time.deltaTime, 0)); // 이동시킬거에요 -> 수직 방향으로만 캐릭터를
|
||||||
// CheckAbnormalHeight(); // 넉백되다가 날아가지 않게 체크
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateAnimation() // 함수를 선언할거에요 -> 애니메이션 파라미터를 조절하는 UpdateAnimation을
|
||||||
/// ⭐ 비정상 높이 감지 및 보정
|
|
||||||
|
|
||||||
//private void CheckAbnormalHeight()
|
|
||||||
//{
|
|
||||||
// // 내 발밑으로 레이저를 쏴서 땅까지의 거리를 잽니다.
|
|
||||||
// RaycastHit hit;
|
|
||||||
// if (Physics.Raycast(transform.position, Vector3.down, out hit, 100f))
|
|
||||||
// {
|
|
||||||
// float heightAboveGround = transform.position.y - hit.point.y;
|
|
||||||
|
|
||||||
// // 땅에서 3미터 이상 떠있는데, 시스템상으론 점프 상태가 아니라면? (버그!)
|
|
||||||
// if (heightAboveGround > 3f && !_controller.isGrounded)
|
|
||||||
// {
|
|
||||||
// // 강제로 땅바닥 위치로 좌표를 계산
|
|
||||||
// Vector3 correctedPos = transform.position;
|
|
||||||
// correctedPos.y = hit.point.y + _controller.height / 2f + minGroundHeight;
|
|
||||||
|
|
||||||
// // 순간이동 시킬 땐 CharacterController를 잠시 꺼야 안전함
|
|
||||||
// _controller.enabled = false;
|
|
||||||
// transform.position = correctedPos;
|
|
||||||
// _controller.enabled = true;
|
|
||||||
|
|
||||||
// _verticalVelocity = 0f; // 낙하 속도 초기화
|
|
||||||
|
|
||||||
// Debug.LogWarning("[PlayerMovement] 비정상적인 높이 감지! 위치 보정함");
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
private void UpdateAnimation()
|
|
||||||
{
|
{
|
||||||
if (pAnim == null) return;
|
if (pAnim == null) return; // 조건이 맞으면 중단할거에요 -> 애니메이터 스크립트가 없다면
|
||||||
// 이동 입력이 있으면 1(달리기) 또는 0.5(걷기), 없으면 0(대기)
|
|
||||||
float animVal = _moveDir.magnitude > 0.1f ? (_isSprinting ? 1.0f : 0.5f) : 0f;
|
float animVal = _moveDir.magnitude > 0.1f ? (_isSprinting ? 1.0f : 0.5f) : 0f; // 값을 결정할거에요 -> 움직임이 있으면 (달리기면 1.0, 걷기면 0.5), 없으면 0을 animVal에
|
||||||
// 차징 중이면 무조건 걷기 모션(0.5) 이하로 만듦
|
|
||||||
if (attackScript != null && attackScript.IsCharging) animVal *= 0.5f;
|
if (attackScript != null && attackScript.IsCharging) animVal *= 0.5f; // 조건이 맞으면 실행할거에요 -> 차징 중이라면 애니메이션 속도를 절반으로 줄여서
|
||||||
pAnim.UpdateMove(animVal);
|
|
||||||
|
pAnim.UpdateMove(animVal); // 함수를 실행할거에요 -> 계산된 애니메이션 값(animVal)을 전달하는 UpdateMove를
|
||||||
}
|
}
|
||||||
|
|
||||||
// 대시 관련
|
// 대시 관련
|
||||||
private IEnumerator DashRoutine()
|
private IEnumerator DashRoutine() // 코루틴 함수를 선언할거에요 -> 대시 로직을 수행할 DashRoutine을
|
||||||
{
|
{
|
||||||
_isDashing = true;
|
_isDashing = true; // 상태를 바꿀거에요 -> 대시 중 상태를 참(true)으로
|
||||||
_lastDashTime = Time.time;
|
_lastDashTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 대시 시간으로
|
||||||
|
|
||||||
// 이동 중이면 그 방향으로, 멈춰있으면 뒤로 회피(백스탭)
|
// 이동 중이면 그 방향으로, 멈춰있으면 뒤로 회피(백스탭)
|
||||||
Vector3 dashDir = _moveDir.sqrMagnitude > 0.001f ? _moveDir : -transform.forward;
|
Vector3 dashDir = _moveDir.sqrMagnitude > 0.001f ? _moveDir : -transform.forward; // 벡터를 결정할거에요 -> 이동 중이면 이동 방향, 아니면 뒤쪽 방향을 dashDir에
|
||||||
|
|
||||||
// 무적 판정 켜기 (소울류 게임 회피 느낌)
|
// 무적 판정 켜기 (소울류 게임 회피 느낌)
|
||||||
if (health != null) health.isInvincible = true;
|
if (health != null) health.isInvincible = true; // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있다면 무적 상태를 켜기(true)로
|
||||||
|
|
||||||
float startTime = Time.time;
|
float startTime = Time.time; // 값을 저장할거에요 -> 대시 시작 시간을 startTime에
|
||||||
// 정해진 시간(0.08초) 동안 반복
|
|
||||||
while (Time.time < startTime + dashDuration)
|
while (Time.time < startTime + dashDuration) // 반복할거에요 -> 현재 시간이 (시작시간 + 대시지속시간)보다 작을 동안
|
||||||
{
|
{
|
||||||
// 속도 = 거리 / 시간
|
float speed = dashDistance / dashDuration; // 값을 계산할거에요 -> 거리 나누기 시간으로 대시 속도를 구해서 speed에
|
||||||
float speed = dashDistance / dashDuration;
|
Vector3 dashMotion = dashDir * speed * Time.deltaTime; // 벡터를 계산할거에요 -> 대시 방향 * 속도 * 시간을 곱해 이동량을 dashMotion에
|
||||||
Vector3 dashMotion = dashDir * speed * Time.deltaTime;
|
|
||||||
|
|
||||||
// ⭐ transform.position += ... 대신 Move()를 쓰는 이유:
|
// ⭐ transform.position += ... 대신 Move()를 쓰는 이유:
|
||||||
// Move를 써야 대시 도중 벽을 만나면 뚫지 않고 멈줌
|
// Move를 써야 대시 도중 벽을 만나면 뚫지 않고 멈줌
|
||||||
CollisionFlags flags = _controller.Move(dashMotion);
|
CollisionFlags flags = _controller.Move(dashMotion); // 이동시킬거에요 -> 캐릭터 컨트롤러를 대시 이동량만큼
|
||||||
|
|
||||||
yield return null; // 한 프레임 대기
|
yield return null; // 대기할거에요 -> 다음 프레임까지 한 턴을
|
||||||
}
|
}
|
||||||
|
|
||||||
// 무적 끄기 및 상태 해제
|
// 무적 끄기 및 상태 해제
|
||||||
if (health != null) health.isInvincible = false;
|
if (health != null) health.isInvincible = false; // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있다면 무적 상태를 끄기(false)로
|
||||||
_isDashing = false;
|
_isDashing = false; // 상태를 바꿀거에요 -> 대시 중 상태를 거짓(false)으로
|
||||||
}
|
}
|
||||||
|
|
||||||
// 다른 스크립트에서 상태를 물어볼 때 쓰는 함수들
|
// 다른 스크립트에서 상태를 물어볼 때 쓰는 함수들
|
||||||
public bool IsGrounded() => _controller.isGrounded;
|
public bool IsGrounded() => _controller.isGrounded; // 값을 반환할거에요 -> 캐릭터가 땅에 닿아있는지 여부를
|
||||||
public bool IsDashing() => _isDashing;
|
public bool IsDashing() => _isDashing; // 값을 반환할거에요 -> 현재 대시 중인지 여부를
|
||||||
public float GetCurrentSpeed() => _controller.velocity.magnitude;
|
public float GetCurrentSpeed() => _controller.velocity.magnitude; // 값을 반환할거에요 -> 현재 캐릭터의 실제 이동 속도 크기를
|
||||||
}
|
}
|
||||||
|
|
@ -1,41 +1,41 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 플레이어의 시각(VFX) 및 청각(SFX) 효과를 전담하는 스크립트
|
/// 플레이어의 시각(VFX) 및 청각(SFX) 효과를 전담하는 스크립트
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PlayerEffects : MonoBehaviour
|
public class PlayerEffects : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerEffects를
|
||||||
{
|
{
|
||||||
[Header("--- 시각 효과 (VFX) ---")]
|
[Header("--- 시각 효과 (VFX) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 시각 효과 (VFX) --- 를
|
||||||
[SerializeField] private GameObject[] slashEffects; // 콤보별 다른 이펙트 가능
|
[SerializeField] private GameObject[] slashEffects; // 배열을 선언할거에요 -> 베기 이펙트 프리팹들을
|
||||||
[SerializeField] private Transform slashSpawnPoint;
|
[SerializeField] private Transform slashSpawnPoint; // 변수를 선언할거에요 -> 이펙트 생성 위치를
|
||||||
|
|
||||||
[Header("--- 청각 효과 (SFX) ---")]
|
[Header("--- 청각 효과 (SFX) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 청각 효과 (SFX) --- 를
|
||||||
[SerializeField] private AudioClip[] swingSounds;
|
[SerializeField] private AudioClip[] swingSounds; // 배열을 선언할거에요 -> 휘두르는 소리 클립들을
|
||||||
private AudioSource _audioSource;
|
private AudioSource _audioSource; // 변수를 선언할거에요 -> 오디오 소스 컴포넌트를
|
||||||
|
|
||||||
private void Awake()
|
private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를
|
||||||
{
|
{
|
||||||
_audioSource = GetComponent<AudioSource>();
|
_audioSource = GetComponent<AudioSource>(); // 컴포넌트를 가져올거에요 -> 오디오 소스를
|
||||||
if (_audioSource == null) _audioSource = gameObject.AddComponent<AudioSource>();
|
if (_audioSource == null) _audioSource = gameObject.AddComponent<AudioSource>(); // 없으면 추가할거에요 -> 오디오 소스 컴포넌트를
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 애니메이션 이벤트에서 호출할 함수
|
/// 애니메이션 이벤트에서 호출할 함수
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="comboIndex">현재 공격 콤보 번호 (0~2)</param>
|
/// <param name="comboIndex">현재 공격 콤보 번호 (0~2)</param>
|
||||||
public void PlaySlashEffect(int comboIndex)
|
public void PlaySlashEffect(int comboIndex) // 함수를 선언할거에요 -> 베기 이펙트를 재생하는 PlaySlashEffect를
|
||||||
{
|
{
|
||||||
// 1. 사운드 재생
|
// 1. 사운드 재생
|
||||||
if (swingSounds.Length > comboIndex && swingSounds[comboIndex] != null)
|
if (swingSounds.Length > comboIndex && swingSounds[comboIndex] != null) // 조건이 맞으면 실행할거에요 -> 해당 인덱스의 소리가 있다면
|
||||||
{
|
{
|
||||||
_audioSource.PlayOneShot(swingSounds[comboIndex]);
|
_audioSource.PlayOneShot(swingSounds[comboIndex]); // 재생할거에요 -> 해당 오디오 클립을
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 이펙트 생성
|
// 2. 이펙트 생성
|
||||||
if (slashEffects.Length > comboIndex && slashEffects[comboIndex] != null && slashSpawnPoint != null)
|
if (slashEffects.Length > comboIndex && slashEffects[comboIndex] != null && slashSpawnPoint != null) // 조건이 맞으면 실행할거에요 -> 해당 인덱스의 이펙트와 생성 위치가 있다면
|
||||||
{
|
{
|
||||||
GameObject slash = Instantiate(slashEffects[comboIndex], slashSpawnPoint.position, slashSpawnPoint.rotation);
|
GameObject slash = Instantiate(slashEffects[comboIndex], slashSpawnPoint.position, slashSpawnPoint.rotation); // 생성할거에요 -> 이펙트 오브젝트를 생성 위치에
|
||||||
Destroy(slash, 1.0f);
|
Destroy(slash, 1.0f); // 파괴할거에요 -> 생성된 이펙트를 1초 뒤에
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,93 +1,101 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
|
||||||
|
|
||||||
public class EquippableItem : MonoBehaviour
|
public class EquippableItem : MonoBehaviour // 클래스를 선언할거에요 -> 장착/드랍/투척 가능한 아이템을 처리하는 EquippableItem을
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> EquippableItem 범위를
|
||||||
[SerializeField] private WeaponConfig config;
|
|
||||||
public WeaponConfig Config => config;
|
|
||||||
|
|
||||||
[Header("--- 데미지 밸런스 ---")]
|
[SerializeField] private WeaponConfig config; // 변수를 선언할거에요 -> 이 아이템이 참조할 무기 설정(WeaponConfig)을 config에
|
||||||
[SerializeField] private float lv1Mult = 1.0f;
|
public WeaponConfig Config => config; // 프로퍼티를 선언할거에요 -> 외부에서 config를 읽을 수 있게 Config로 노출
|
||||||
[SerializeField] private float lv2Mult = 1.5f;
|
|
||||||
[SerializeField] private float lv3Mult = 2.5f;
|
|
||||||
// [제거] strengthBonusFactor 변수 삭제
|
|
||||||
|
|
||||||
private Rigidbody _rb;
|
[Header("--- 데미지 밸런스 ---")] // 인스펙터에 제목을 표시할거에요 -> 데미지 밸런스 섹션을
|
||||||
private Collider _col;
|
[SerializeField] private float lv1Mult = 1.0f; // 변수를 선언할거에요 -> 차지 1단계 배율을 lv1Mult에
|
||||||
private bool _isThrown;
|
[SerializeField] private float lv2Mult = 1.5f; // 변수를 선언할거에요 -> 차지 2단계 배율을 lv2Mult에
|
||||||
private int _chargeLevel;
|
[SerializeField] private float lv3Mult = 2.5f; // 변수를 선언할거에요 -> 차지 3단계 배율을 lv3Mult에
|
||||||
private Stats _thrower;
|
// [제거] strengthBonusFactor 변수 삭제 // 설명을 적을거에요 -> 힘 보너스 배율은 더 이상 사용하지 않음
|
||||||
private Vector3 _originalWorldScale;
|
|
||||||
|
|
||||||
private void Awake()
|
private Rigidbody _rb; // 변수를 선언할거에요 -> 리지드바디를 저장할 _rb를
|
||||||
{
|
private Collider _col; // 변수를 선언할거에요 -> 콜라이더를 저장할 _col을
|
||||||
_rb = GetComponent<Rigidbody>();
|
private bool _isThrown; // 변수를 선언할거에요 -> 현재 투척 상태인지 저장할 _isThrown을
|
||||||
_col = GetComponent<Collider>();
|
private int _chargeLevel; // 변수를 선언할거에요 -> 투척 당시 차지 레벨을 저장할 _chargeLevel을
|
||||||
_originalWorldScale = transform.lossyScale;
|
private Stats _thrower; // 변수를 선언할거에요 -> 투척한 주체(플레이어 스탯)를 저장할 _thrower를
|
||||||
}
|
private Vector3 _originalWorldScale; // 변수를 선언할거에요 -> 원래 월드 스케일을 저장할 _originalWorldScale을
|
||||||
|
|
||||||
public void OnPickedUp(Transform hand)
|
private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> Awake 범위를
|
||||||
_isThrown = false;
|
_rb = GetComponent<Rigidbody>(); // 컴포넌트를 가져올거에요 -> Rigidbody를 찾아 _rb에 저장
|
||||||
transform.SetParent(hand);
|
_col = GetComponent<Collider>(); // 컴포넌트를 가져올거에요 -> Collider를 찾아 _col에 저장
|
||||||
transform.localScale = new Vector3(
|
_originalWorldScale = transform.lossyScale; // 값을 저장할거에요 -> 현재 월드 기준 크기를 원본으로 기억
|
||||||
_originalWorldScale.x / hand.lossyScale.x,
|
} // 코드 블록을 끝낼거에요 -> Awake를
|
||||||
_originalWorldScale.y / hand.lossyScale.y,
|
|
||||||
_originalWorldScale.z / hand.lossyScale.z
|
|
||||||
);
|
|
||||||
transform.localPosition = Vector3.zero;
|
|
||||||
transform.localRotation = Quaternion.identity;
|
|
||||||
if (_rb) _rb.isKinematic = true;
|
|
||||||
if (_col) _col.enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnDropped(Vector3 throwDir)
|
public void OnPickedUp(Transform hand) // 함수를 선언할거에요 -> 아이템을 손에 집었을 때 처리하는 OnPickedUp을
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> OnPickedUp 범위를
|
||||||
_isThrown = false;
|
_isThrown = false; // 상태를 바꿀거에요 -> 투척 상태를 해제(false)로
|
||||||
transform.SetParent(null);
|
transform.SetParent(hand); // 부모를 바꿀거에요 -> 손(Hand) 트랜스폼에 붙이기
|
||||||
transform.localScale = _originalWorldScale;
|
|
||||||
if (_rb)
|
|
||||||
{
|
|
||||||
_rb.isKinematic = false;
|
|
||||||
Vector3 force = (throwDir + Vector3.up * 0.5f).normalized * 5f;
|
|
||||||
_rb.AddForce(force, ForceMode.Impulse);
|
|
||||||
_rb.AddTorque(Random.insideUnitSphere * 3f, ForceMode.Impulse);
|
|
||||||
}
|
|
||||||
if (_col) _col.enabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnThrown(Vector3 dir, float force, int lv, Stats s)
|
transform.localScale = new Vector3( // 값을 설정할거에요 -> 손의 스케일 변화에도 월드 크기 유지되게 로컬 스케일 계산
|
||||||
{
|
_originalWorldScale.x / hand.lossyScale.x, // x축 크기를 보정할거에요 -> 원본 월드 스케일 기준으로
|
||||||
_isThrown = true;
|
_originalWorldScale.y / hand.lossyScale.y, // y축 크기를 보정할거에요 -> 원본 월드 스케일 기준으로
|
||||||
_chargeLevel = lv;
|
_originalWorldScale.z / hand.lossyScale.z // z축 크기를 보정할거에요 -> 원본 월드 스케일 기준으로
|
||||||
_thrower = s;
|
); // 로컬 스케일 설정을 끝낼거에요 -> 월드 크기 유지
|
||||||
transform.SetParent(null);
|
|
||||||
transform.localScale = _originalWorldScale;
|
|
||||||
if (_rb)
|
|
||||||
{
|
|
||||||
_rb.isKinematic = false;
|
|
||||||
_rb.AddForce(dir * force, ForceMode.Impulse);
|
|
||||||
_rb.AddTorque(transform.right * 10f, ForceMode.Impulse);
|
|
||||||
}
|
|
||||||
if (_col) _col.enabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCollisionEnter(Collision collision)
|
transform.localPosition = Vector3.zero; // 위치를 맞출거에요 -> 손 기준 로컬 위치 0으로
|
||||||
{
|
transform.localRotation = Quaternion.identity; // 회전을 맞출거에요 -> 손 기준 로컬 회전 초기화
|
||||||
if (!_isThrown) return;
|
if (_rb) _rb.isKinematic = true; // 조건이 맞으면 실행할거에요 -> 리지드바디가 있으면 물리 비활성(키네마틱)으로
|
||||||
if (_thrower != null && collision.gameObject == _thrower.gameObject) return;
|
if (_col) _col.enabled = false; // 조건이 맞으면 실행할거에요 -> 콜라이더가 있으면 꺼서 손에서 충돌 안 나게
|
||||||
|
} // 코드 블록을 끝낼거에요 -> OnPickedUp을
|
||||||
|
|
||||||
if (collision.gameObject.TryGetComponent<IDamageable>(out var target))
|
public void OnDropped(Vector3 throwDir) // 함수를 선언할거에요 -> 아이템을 떨어뜨릴 때 처리하는 OnDropped를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> OnDropped 범위를
|
||||||
float mult = _chargeLevel == 3 ? lv3Mult : (_chargeLevel == 2 ? lv2Mult : lv1Mult);
|
_isThrown = false; // 상태를 바꿀거에요 -> 투척 상태가 아님(false)으로
|
||||||
|
transform.SetParent(null); // 부모를 해제할거에요 -> 월드에 놓기
|
||||||
|
transform.localScale = _originalWorldScale; // 스케일을 되돌릴거에요 -> 원래 월드 스케일로
|
||||||
|
|
||||||
// ✨ [수정] 힘 보너스 로직 제거. (플레이어 기본 공격력 + 무기 대미지) * 차지 배율
|
if (_rb) // 조건을 검사할거에요 -> 리지드바디가 있는지
|
||||||
float finalDamage = (_thrower.BaseAttackDamage + config.BaseDamage) * mult;
|
{ // 코드 블록을 시작할거에요 -> 물리 드랍 처리
|
||||||
|
_rb.isKinematic = false; // 값을 바꿀거에요 -> 물리 활성화
|
||||||
|
Vector3 force = (throwDir + Vector3.up * 0.5f).normalized * 5f; // 힘을 계산할거에요 -> 던지는 방향 + 살짝 위로 + 세기 5
|
||||||
|
_rb.AddForce(force, ForceMode.Impulse); // 힘을 줄거에요 -> 순간 힘(Impulse)으로 튕기듯 드랍
|
||||||
|
_rb.AddTorque(Random.insideUnitSphere * 3f, ForceMode.Impulse); // 회전도 줄거에요 -> 랜덤 토크로 자연스럽게 굴러가게
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 물리 드랍 처리
|
||||||
|
|
||||||
target.TakeDamage(finalDamage);
|
if (_col) _col.enabled = true; // 조건이 맞으면 실행할거에요 -> 콜라이더를 다시 켜기
|
||||||
Debug.Log($"<color=orange>[투척 적중]</color> {collision.gameObject.name}에게 {finalDamage:F1} 데미지!");
|
} // 코드 블록을 끝낼거에요 -> OnDropped를
|
||||||
_isThrown = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collision.gameObject.layer != LayerMask.NameToLayer("Player")) _isThrown = false;
|
public void OnThrown(Vector3 dir, float force, int lv, Stats s) // 함수를 선언할거에요 -> 투척 시작 세팅을 하는 OnThrown을
|
||||||
}
|
{ // 코드 블록을 시작할거에요 -> OnThrown 범위를
|
||||||
}
|
_isThrown = true; // 상태를 바꿀거에요 -> 투척 상태(true)로
|
||||||
|
_chargeLevel = lv; // 값을 저장할거에요 -> 투척 당시 차지 레벨을
|
||||||
|
_thrower = s; // 값을 저장할거에요 -> 던진 주체(스탯)를 저장
|
||||||
|
transform.SetParent(null); // 부모를 해제할거에요 -> 월드로 분리
|
||||||
|
transform.localScale = _originalWorldScale; // 스케일을 되돌릴거에요 -> 원래 월드 스케일로
|
||||||
|
|
||||||
|
if (_rb) // 조건을 검사할거에요 -> 리지드바디가 있는지
|
||||||
|
{ // 코드 블록을 시작할거에요 -> 물리 투척 처리
|
||||||
|
_rb.isKinematic = false; // 값을 바꿀거에요 -> 물리 활성화
|
||||||
|
_rb.AddForce(dir * force, ForceMode.Impulse); // 힘을 줄거에요 -> 던지는 방향 * force로 순간 가속
|
||||||
|
_rb.AddTorque(transform.right * 10f, ForceMode.Impulse); // 회전을 줄거에요 -> 오른쪽 축으로 스핀 주기
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 물리 투척 처리
|
||||||
|
|
||||||
|
if (_col) _col.enabled = true; // 조건이 맞으면 실행할거에요 -> 콜라이더를 켜서 충돌 가능하게
|
||||||
|
} // 코드 블록을 끝낼거에요 -> OnThrown를
|
||||||
|
|
||||||
|
private void OnCollisionEnter(Collision collision) // 함수를 선언할거에요 -> 충돌이 일어나면 호출되는 OnCollisionEnter를
|
||||||
|
{ // 코드 블록을 시작할거에요 -> OnCollisionEnter 범위를
|
||||||
|
if (!_isThrown) return; // 조건이 맞으면 종료할거에요 -> 투척 상태가 아니면 무시
|
||||||
|
if (_thrower != null && collision.gameObject == _thrower.gameObject) return; // 조건이 맞으면 종료할거에요 -> 던진 본인에게는 맞지 않게
|
||||||
|
|
||||||
|
if (collision.gameObject.TryGetComponent<IDamageable>(out var target)) // 조건을 검사할거에요 -> 맞은 대상이 데미지를 받을 수 있는지
|
||||||
|
{ // 코드 블록을 시작할거에요 -> 데미지 처리
|
||||||
|
float mult = _chargeLevel == 3 ? lv3Mult : (_chargeLevel == 2 ? lv2Mult : lv1Mult); // 배율을 고를거에요 -> 차지 레벨에 따라 lv1/2/3 배율
|
||||||
|
|
||||||
|
// ✨ [수정] 힘 보너스 로직 제거. (플레이어 기본 공격력 + 무기 대미지) * 차지 배율 // 설명을 적을거에요 -> 최종 데미지 계산 공식
|
||||||
|
float finalDamage = (_thrower.BaseAttackDamage + config.BaseDamage) * mult; // 값을 계산할거에요 -> (플레이어 공격력 + 무기 기본 데미지) * 배율
|
||||||
|
|
||||||
|
target.TakeDamage(finalDamage); // 함수를 실행할거에요 -> 대상에게 finalDamage만큼 데미지 주기
|
||||||
|
Debug.Log($"<color=orange>[투척 적중]</color> {collision.gameObject.name}에게 {finalDamage:F1} 데미지!"); // 로그를 찍을거에요 -> 적중 결과 출력
|
||||||
|
_isThrown = false; // 상태를 바꿀거에요 -> 한 번 맞으면 투척 상태 해제
|
||||||
|
} // 코드 블록을 끝낼거에요 -> 데미지 처리
|
||||||
|
|
||||||
|
if (collision.gameObject.layer != LayerMask.NameToLayer("Player")) _isThrown = false; // 조건이 맞으면 실행할거에요 -> 플레이어 레이어가 아니면 투척 상태 해제
|
||||||
|
} // 코드 블록을 끝낼거에요 -> OnCollisionEnter를
|
||||||
|
|
||||||
|
} // 코드 블록을 끝낼거에요 -> EquippableItem을
|
||||||
|
|
@ -1,54 +1,54 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
public class HealthAltar : MonoBehaviour
|
public class HealthAltar : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 HealthAltar를
|
||||||
{
|
{
|
||||||
[Header("--- 회복 설정 ---")]
|
[Header("--- 회복 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 회복 설정 --- 을
|
||||||
[SerializeField] private float healAmount = 50f; // 상호작용 시 즉시 회복량
|
[SerializeField] private float healAmount = 50f; // 변수를 선언할거에요 -> 회복량인 healAmount를
|
||||||
[SerializeField] private float interactRange = 3.5f; // 상호작용 가능 거리
|
[SerializeField] private float interactRange = 3.5f; // 변수를 선언할거에요 -> 상호작용 거리인 interactRange를
|
||||||
|
|
||||||
[Header("--- 시각 효과 ---")]
|
[Header("--- 시각 효과 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 시각 효과 --- 를
|
||||||
[SerializeField] private ParticleSystem healEffect;
|
[SerializeField] private ParticleSystem healEffect; // 변수를 선언할거에요 -> 회복 이펙트인 healEffect를
|
||||||
|
|
||||||
// ⭐ [핵심 수정] PlayerInteraction에서 던져주는 'PlayerHealth' 자료형을 직접 받습니다!
|
// ⭐ [핵심 수정] PlayerInteraction에서 던져주는 'PlayerHealth' 자료형을 직접 받습니다!
|
||||||
//
|
//
|
||||||
public void Use(PlayerHealth playerHealth)
|
public void Use(PlayerHealth playerHealth) // 함수를 선언할거에요 -> 제단을 사용하는 Use를
|
||||||
{
|
{
|
||||||
if (playerHealth == null) return;
|
if (playerHealth == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어 체력 스크립트가 없다면
|
||||||
|
|
||||||
// 플레이어의 Transform 정보 추출
|
// 플레이어의 Transform 정보 추출
|
||||||
Transform interactor = playerHealth.transform;
|
Transform interactor = playerHealth.transform; // 값을 가져올거에요 -> 상호작용하는 대상의 위치 정보를
|
||||||
|
|
||||||
// 규칙: 실제 몸통 중심점($Center$)과의 거리 계산
|
// 규칙: 실제 몸통 중심점($Center$)과의 거리 계산
|
||||||
float distance = Vector3.Distance(transform.position, interactor.position);
|
float distance = Vector3.Distance(transform.position, interactor.position); // 거리를 계산할거에요 -> 제단과 플레이어 사이의 거리를
|
||||||
|
|
||||||
if (distance <= interactRange)
|
if (distance <= interactRange) // 조건이 맞으면 실행할거에요 -> 거리가 상호작용 범위 이내라면
|
||||||
{
|
{
|
||||||
ApplyHeal(playerHealth); // 컴포넌트 자체를 넘겨줌
|
ApplyHeal(playerHealth); // 함수를 실행할거에요 -> 회복 적용 함수 ApplyHeal을 // 컴포넌트 자체를 넘겨줌
|
||||||
}
|
}
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 거리가 너무 멀다면
|
||||||
{
|
{
|
||||||
Debug.Log($"[제단] 너무 멉니다! (거리: {distance:F1} / 제한: {interactRange})");
|
Debug.Log($"[제단] 너무 멉니다! (거리: {distance:F1} / 제한: {interactRange})"); // 로그를 출력할거에요 -> 거리 부족 메시지를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyHeal(PlayerHealth targetHealth)
|
private void ApplyHeal(PlayerHealth targetHealth) // 함수를 선언할거에요 -> 실제로 체력을 회복시키는 ApplyHeal을
|
||||||
{
|
{
|
||||||
// ⭐ 캡슐화 규칙: 플레이어의 Health 스크립트에 있는 기능을 호출합니다.
|
// ⭐ 캡슐화 규칙: 플레이어의 Health 스크립트에 있는 기능을 호출합니다.
|
||||||
// 유저님의 프로젝트에 정의된 회복 함수 이름을 사용하세요 (예: RestoreHealth, AddHealth 등)
|
// 유저님의 프로젝트에 정의된 회복 함수 이름을 사용하세요 (예: RestoreHealth, AddHealth 등)
|
||||||
// targetHealth.RestoreHealth(healAmount); //
|
targetHealth.Heal(healAmount); // 함수를 실행할거에요 -> 플레이어의 회복 함수인 Heal을 //
|
||||||
|
|
||||||
if (healEffect != null)
|
if (healEffect != null) // 조건이 맞으면 실행할거에요 -> 이펙트가 설정되어 있다면
|
||||||
{
|
{
|
||||||
healEffect.transform.position = targetHealth.transform.position;
|
healEffect.transform.position = targetHealth.transform.position; // 위치를 옮길거에요 -> 플레이어 위치로
|
||||||
healEffect.Play();
|
healEffect.Play(); // 실행할거에요 -> 이펙트 재생을
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.Log($"[제단] {targetHealth.gameObject.name} 상호작용 성공! {healAmount} HP 회복.");
|
Debug.Log($"[제단] {targetHealth.gameObject.name} 상호작용 성공! {healAmount} HP 회복."); // 로그를 출력할거에요 -> 회복 성공 메시지를
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDrawGizmosSelected()
|
private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 선택 시 기즈모를 그리는 OnDrawGizmosSelected를
|
||||||
{
|
{
|
||||||
Gizmos.color = Color.cyan;
|
Gizmos.color = Color.cyan; // 색상을 설정할거에요 -> 하늘색으로
|
||||||
Gizmos.DrawWireSphere(transform.position, interactRange);
|
Gizmos.DrawWireSphere(transform.position, interactRange); // 그림을 그릴거에요 -> 상호작용 범위를 표시하는 원을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,44 +1,44 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
public class PlayerInteraction : MonoBehaviour
|
public class PlayerInteraction : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerInteraction을
|
||||||
{
|
{
|
||||||
[Header("--- 설정 ---")]
|
[Header("--- 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 설정 --- 을
|
||||||
[SerializeField] private float interactRange = 3f;
|
[SerializeField] private float interactRange = 3f; // 변수를 선언할거에요 -> 상호작용 거리인 interactRange를
|
||||||
[SerializeField] private LayerMask itemLayer;
|
[SerializeField] private LayerMask itemLayer; // 변수를 선언할거에요 -> 아이템 레이어인 itemLayer를
|
||||||
[SerializeField] private Transform handSlot;
|
[SerializeField] private Transform handSlot; // 변수를 선언할거에요 -> 무기 장착 위치인 handSlot을
|
||||||
[SerializeField] private Stats playerStats;
|
[SerializeField] private Stats playerStats; // 변수를 선언할거에요 -> 플레이어 스탯 스크립트인 playerStats를
|
||||||
|
|
||||||
private EquippableItem _currentWeapon;
|
private EquippableItem _currentWeapon; // 변수를 선언할거에요 -> 현재 장착된 무기인 _currentWeapon을
|
||||||
public EquippableItem CurrentWeapon => _currentWeapon;
|
public EquippableItem CurrentWeapon => _currentWeapon; // 프로퍼티를 선언할거에요 -> 현재 무기를 반환하는 CurrentWeapon을
|
||||||
|
|
||||||
public void TryInteract()
|
public void TryInteract() // 함수를 선언할거에요 -> 상호작용을 시도하는 TryInteract를
|
||||||
{
|
{
|
||||||
Collider[] hits = Physics.OverlapSphere(transform.position, interactRange, itemLayer);
|
Collider[] hits = Physics.OverlapSphere(transform.position, interactRange, itemLayer); // 배열을 만들거에요 -> 주변 아이템들을 감지해서 hits에
|
||||||
foreach (var hit in hits)
|
foreach (var hit in hits) // 반복할거에요 -> 감지된 모든 아이템에 대해
|
||||||
{
|
{
|
||||||
// [MODIFIED] ArrowItem 제거 → ArrowPickup으로 통일
|
// [MODIFIED] ArrowItem 제거 → ArrowPickup으로 통일
|
||||||
if (hit.TryGetComponent<ArrowPickup>(out var arrowPickup))
|
if (hit.TryGetComponent<ArrowPickup>(out var arrowPickup)) // 조건이 맞으면 실행할거에요 -> 대상이 화살 아이템이라면
|
||||||
{
|
{
|
||||||
PickupArrow(arrowPickup);
|
PickupArrow(arrowPickup); // 함수를 실행할거에요 -> 화살 줍기 함수 PickupArrow를
|
||||||
break;
|
break; // 중단할거에요 -> 반복문을 (하나만 줍기 위해)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hit.TryGetComponent<EquippableItem>(out var item))
|
if (hit.TryGetComponent<EquippableItem>(out var item)) // 조건이 맞으면 실행할거에요 -> 대상이 장착 가능한 무기라면
|
||||||
{
|
{
|
||||||
EquipWeapon(item);
|
EquipWeapon(item); // 함수를 실행할거에요 -> 무기 장착 함수 EquipWeapon을
|
||||||
break;
|
break; // 중단할거에요 -> 반복문을
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hit.TryGetComponent<HealthPotion>(out var potion))
|
if (hit.TryGetComponent<HealthPotion>(out var potion)) // 조건이 맞으면 실행할거에요 -> 대상이 회복 물약이라면
|
||||||
{
|
{
|
||||||
potion.Use(GetComponent<PlayerHealth>());
|
potion.Use(GetComponent<PlayerHealth>()); // 함수를 실행할거에요 -> 물약 사용 함수 Use를
|
||||||
break;
|
break; // 중단할거에요 -> 반복문을
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hit.TryGetComponent<HealthAltar>(out var altar))
|
if (hit.TryGetComponent<HealthAltar>(out var altar)) // 조건이 맞으면 실행할거에요 -> 대상이 회복 제단이라면
|
||||||
{
|
{
|
||||||
altar.Use(GetComponent<PlayerHealth>());
|
altar.Use(GetComponent<PlayerHealth>()); // 함수를 실행할거에요 -> 제단 사용 함수 Use를
|
||||||
break;
|
break; // 중단할거에요 -> 반복문을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -47,47 +47,47 @@ public class PlayerInteraction : MonoBehaviour
|
||||||
/// [MODIFIED] ArrowPickup을 직접 처리
|
/// [MODIFIED] ArrowPickup을 직접 처리
|
||||||
/// 기존: ArrowItem 타입 → 변경: ArrowPickup 타입으로 통일
|
/// 기존: ArrowItem 타입 → 변경: ArrowPickup 타입으로 통일
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void PickupArrow(ArrowPickup arrowPickup)
|
private void PickupArrow(ArrowPickup arrowPickup) // 함수를 선언할거에요 -> 화살을 줍는 PickupArrow를
|
||||||
{
|
{
|
||||||
if (arrowPickup == null) return;
|
if (arrowPickup == null) return; // 조건이 맞으면 중단할거에요 -> 화살 아이템이 없다면
|
||||||
|
|
||||||
PlayerAttack playerAttack = GetComponent<PlayerAttack>();
|
PlayerAttack playerAttack = GetComponent<PlayerAttack>(); // 컴포넌트를 찾을거에요 -> 플레이어 공격 스크립트를
|
||||||
if (playerAttack != null)
|
if (playerAttack != null) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있다면
|
||||||
{
|
{
|
||||||
// ArrowPickup.Pickup() 내부에서
|
// ArrowPickup.Pickup() 내부에서
|
||||||
// playerAttack.SetCurrentArrow(ArrowData)를 호출합니다
|
// playerAttack.SetCurrentArrow(ArrowData)를 호출합니다
|
||||||
arrowPickup.Pickup(playerAttack);
|
arrowPickup.Pickup(playerAttack); // 실행할거에요 -> 화살 아이템의 줍기 함수를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EquipWeapon(EquippableItem item)
|
private void EquipWeapon(EquippableItem item) // 함수를 선언할거에요 -> 무기를 장착하는 EquipWeapon을
|
||||||
{
|
{
|
||||||
if (_currentWeapon != null) _currentWeapon.OnDropped(transform.forward);
|
if (_currentWeapon != null) _currentWeapon.OnDropped(transform.forward); // 조건이 맞으면 실행할거에요 -> 이미 든 무기가 있다면 떨구기를
|
||||||
_currentWeapon = item;
|
_currentWeapon = item; // 값을 저장할거에요 -> 새 무기를 현재 무기로
|
||||||
_currentWeapon.OnPickedUp(handSlot);
|
_currentWeapon.OnPickedUp(handSlot); // 실행할거에요 -> 무기의 줍기 함수 OnPickedUp을
|
||||||
|
|
||||||
if (playerStats != null)
|
if (playerStats != null) // 조건이 맞으면 실행할거에요 -> 플레이어 스탯이 있다면
|
||||||
{
|
{
|
||||||
playerStats.weaponDamage = item.Config.BaseDamage;
|
playerStats.weaponDamage = item.Config.BaseDamage; // 값을 설정할거에요 -> 무기 공격력을 스탯에 반영하기를
|
||||||
}
|
}
|
||||||
|
|
||||||
FindObjectOfType<PlayerStatsUI>()?.UpdateStatTexts();
|
FindObjectOfType<PlayerStatsUI>()?.UpdateStatTexts(); // 실행할거에요 -> 스탯 UI 갱신을
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearCurrentWeapon()
|
public void ClearCurrentWeapon() // 함수를 선언할거에요 -> 현재 무기를 해제하는 ClearCurrentWeapon을
|
||||||
{
|
{
|
||||||
_currentWeapon = null;
|
_currentWeapon = null; // 값을 비울거에요 -> 현재 무기 변수를
|
||||||
if (playerStats != null)
|
if (playerStats != null) // 조건이 맞으면 실행할거에요 -> 스탯 스크립트가 있다면
|
||||||
{
|
{
|
||||||
playerStats.weaponDamage = 0;
|
playerStats.weaponDamage = 0; // 값을 초기화할거에요 -> 무기 공격력을 0으로
|
||||||
FindObjectOfType<PlayerStatsUI>()?.UpdateStatTexts();
|
FindObjectOfType<PlayerStatsUI>()?.UpdateStatTexts(); // 실행할거에요 -> 스탯 UI 갱신을
|
||||||
}
|
}
|
||||||
|
|
||||||
// [NEW] 무기 해제 시 화살도 초기화
|
// [NEW] 무기 해제 시 화살도 초기화
|
||||||
PlayerAttack playerAttack = GetComponent<PlayerAttack>();
|
PlayerAttack playerAttack = GetComponent<PlayerAttack>(); // 컴포넌트를 찾을거에요 -> 공격 스크립트를
|
||||||
if (playerAttack != null)
|
if (playerAttack != null) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있다면
|
||||||
{
|
{
|
||||||
playerAttack.ResetArrow();
|
playerAttack.ResetArrow(); // 실행할거에요 -> 화살 초기화 함수 ResetArrow를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,70 +1,70 @@
|
||||||
using System.Collections;
|
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
|
||||||
using TMPro;
|
using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using UnityEngine.UI;
|
using UnityEngine.UI; // UI 기능을 사용할거에요 -> UnityEngine.UI를
|
||||||
|
|
||||||
public class PlayerLevelSystem : MonoBehaviour
|
public class PlayerLevelSystem : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerLevelSystem을
|
||||||
{
|
{
|
||||||
[Header("--- 참조 ---")]
|
[Header("--- 참조 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 참조 --- 를
|
||||||
[SerializeField] private Stats stats;
|
[SerializeField] private Stats stats; // 변수를 선언할거에요 -> 스탯 스크립트인 stats를
|
||||||
[SerializeField] private PlayerHealth pHealth;
|
[SerializeField] private PlayerHealth pHealth; // 변수를 선언할거에요 -> 체력 스크립트인 pHealth를
|
||||||
|
|
||||||
[Header("레벨 설정")]
|
[Header("레벨 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 레벨 설정 을
|
||||||
public int level = 1;
|
public int level = 1; // 변수를 초기화할거에요 -> 현재 레벨을 1로
|
||||||
public int currentExp = 0;
|
public int currentExp = 0; // 변수를 초기화할거에요 -> 현재 경험치를 0으로
|
||||||
[SerializeField] private int[] expTable;
|
[SerializeField] private int[] expTable; // 배열을 선언할거에요 -> 레벨별 필요 경험치 테이블인 expTable을
|
||||||
|
|
||||||
[Header("UI")]
|
[Header("UI")] // 인스펙터 창에 제목을 표시할거에요 -> UI 를
|
||||||
[SerializeField] private Image expFillImage;
|
[SerializeField] private Image expFillImage; // 변수를 선언할거에요 -> 경험치바 이미지인 expFillImage를
|
||||||
[SerializeField] private TextMeshProUGUI expText;
|
[SerializeField] private TextMeshProUGUI expText; // 변수를 선언할거에요 -> 경험치 텍스트인 expText를
|
||||||
[SerializeField] private TextMeshProUGUI levelText;
|
[SerializeField] private TextMeshProUGUI levelText; // 변수를 선언할거에요 -> 레벨 텍스트인 levelText를
|
||||||
|
|
||||||
public static System.Action OnLevelUp;
|
public static System.Action OnLevelUp; // 이벤트를 선언할거에요 -> 레벨업 시 호출될 정적 이벤트 OnLevelUp을
|
||||||
|
|
||||||
private int RequiredExp
|
private int RequiredExp // 프로퍼티를 선언할거에요 -> 필요 경험치를 반환하는 RequiredExp를
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
int index = level - 1;
|
int index = level - 1; // 인덱스를 계산할거에요 -> 레벨에서 1을 뺀 값으로
|
||||||
if (index >= expTable.Length) return expTable[expTable.Length - 1];
|
if (index >= expTable.Length) return expTable[expTable.Length - 1]; // 조건이 맞으면 반환할거에요 -> 만렙이면 마지막 필요 경험치를
|
||||||
return expTable[index];
|
return expTable[index]; // 반환할거에요 -> 현재 레벨의 필요 경험치를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnEnable() { MonsterClass.OnMonsterKilled += GainExp; UpdateExpUI(); }
|
private void OnEnable() { MonsterClass.OnMonsterKilled += GainExp; UpdateExpUI(); } // 함수를 실행할거에요 -> 몬스터 처치 이벤트 구독과 UI 갱신을
|
||||||
private void OnDisable() { MonsterClass.OnMonsterKilled -= GainExp; }
|
private void OnDisable() { MonsterClass.OnMonsterKilled -= GainExp; } // 함수를 실행할거에요 -> 몬스터 처치 이벤트 구독 해제를
|
||||||
|
|
||||||
void GainExp(int amount)
|
void GainExp(int amount) // 함수를 선언할거에요 -> 경험치를 획득하는 GainExp를
|
||||||
{
|
{
|
||||||
currentExp += amount;
|
currentExp += amount; // 값을 더할거에요 -> 획득한 경험치를 현재 경험치에
|
||||||
while (currentExp >= RequiredExp) { currentExp -= RequiredExp; LevelUp(); }
|
while (currentExp >= RequiredExp) { currentExp -= RequiredExp; LevelUp(); } // 반복할거에요 -> 경험치가 꽉 찼다면 레벨업을
|
||||||
UpdateExpUI();
|
UpdateExpUI(); // 실행할거에요 -> UI 갱신 함수를
|
||||||
}
|
}
|
||||||
|
|
||||||
void LevelUp()
|
void LevelUp() // 함수를 선언할거에요 -> 레벨업 처리를 하는 LevelUp을
|
||||||
{
|
{
|
||||||
if (level >= expTable.Length + 1) { currentExp = 0; return; }
|
if (level >= expTable.Length + 1) { currentExp = 0; return; } // 조건이 맞으면 중단할거에요 -> 최대 레벨이라면 경험치만 비우고 리턴
|
||||||
level++;
|
level++; // 값을 증가시킬거에요 -> 레벨을 1만큼
|
||||||
|
|
||||||
// ✨ 힘 대신 공격력(+10) 증가
|
// ✨ 힘 대신 공격력(+10) 증가
|
||||||
if (stats != null) stats.AddBaseLevelUpStats(1000f, 10f);
|
if (stats != null) stats.AddBaseLevelUpStats(1000f, 10f); // 실행할거에요 -> 스탯 증가 함수를 (체력 1000, 공격력 10)
|
||||||
|
|
||||||
if (pHealth != null) pHealth.RefreshHealthUI();
|
if (pHealth != null) pHealth.RefreshHealthUI(); // 실행할거에요 -> 체력 UI 갱신을
|
||||||
StartCoroutine(DelayedCardPopup());
|
StartCoroutine(DelayedCardPopup()); // 코루틴을 시작할거에요 -> 카드 선택 팝업 지연 실행을
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerator DelayedCardPopup()
|
private IEnumerator DelayedCardPopup() // 코루틴 함수를 선언할거에요 -> 카드 팝업을 지연시키는 DelayedCardPopup을
|
||||||
{
|
{
|
||||||
yield return new WaitForSeconds(1.5f);
|
yield return new WaitForSeconds(1.5f); // 기다릴거에요 -> 1.5초 동안
|
||||||
OnLevelUp?.Invoke();
|
OnLevelUp?.Invoke(); // 이벤트를 실행할거에요 -> 레벨업 알림 이벤트를
|
||||||
}
|
}
|
||||||
|
|
||||||
void UpdateExpUI()
|
void UpdateExpUI() // 함수를 선언할거에요 -> 경험치 UI를 갱신하는 UpdateExpUI를
|
||||||
{
|
{
|
||||||
float fill = (float)currentExp / RequiredExp;
|
float fill = (float)currentExp / RequiredExp; // 비율을 계산할거에요 -> 현재 경험치 나누기 필요 경험치로
|
||||||
if (expFillImage != null) expFillImage.fillAmount = fill;
|
if (expFillImage != null) expFillImage.fillAmount = fill; // 값을 설정할거에요 -> 게이지 채움 정도를
|
||||||
if (expText != null) expText.text = $"{currentExp} / {RequiredExp}";
|
if (expText != null) expText.text = $"{currentExp} / {RequiredExp}"; // 텍스트를 바꿀거에요 -> 현재/필요 경험치 수치로
|
||||||
if (levelText != null) levelText.text = $"Lv. {level}";
|
if (levelText != null) levelText.text = $"Lv. {level}"; // 텍스트를 바꿀거에요 -> 현재 레벨로
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
|
public enum StatType // 열거형을 선언할거에요 -> 스탯 종류를 구분하는 StatType을
|
||||||
public enum StatType
|
{ // 코드 블록을 시작할거에요 -> StatType 범위를
|
||||||
{
|
Health, // 값을 정의할거에요 -> 체력 스탯을
|
||||||
Health,
|
Speed, // 값을 정의할거에요 -> 이동 속도 스탯을
|
||||||
Speed,
|
// Strength, // 주석 처리할거에요 -> 힘 스탯(현재 미사용)
|
||||||
// Strength,
|
Damage // 값을 정의할거에요 -> 공격 데미지 스탯을
|
||||||
Damage
|
} // 코드 블록을 끝낼거에요 -> StatType을
|
||||||
}
|
|
||||||
|
|
@ -1,56 +1,56 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
public class Stats : MonoBehaviour
|
public class Stats : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 Stats를
|
||||||
{
|
{
|
||||||
[Header("--- 기본 능력치 ---")]
|
[Header("--- 기본 능력치 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 기본 능력치 --- 를
|
||||||
[SerializeField] private float baseMaxHealth = 100f;
|
[SerializeField] private float baseMaxHealth = 100f; // 변수를 선언할거에요 -> 기본 최대 체력인 baseMaxHealth를
|
||||||
[SerializeField] private float baseMoveSpeed = 5f;
|
[SerializeField] private float baseMoveSpeed = 5f; // 변수를 선언할거에요 -> 기본 이동 속도인 baseMoveSpeed를
|
||||||
[SerializeField] private float baseAttackDamage = 10f;
|
[SerializeField] private float baseAttackDamage = 10f; // 변수를 선언할거에요 -> 기본 공격력인 baseAttackDamage를
|
||||||
|
|
||||||
[Header("--- 보너스 능력치 ---")]
|
[Header("--- 보너스 능력치 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 보너스 능력치 --- 를
|
||||||
public float bonusMaxHealth;
|
public float bonusMaxHealth; // 변수를 선언할거에요 -> 추가 체력인 bonusMaxHealth를
|
||||||
public float bonusMoveSpeed;
|
public float bonusMoveSpeed; // 변수를 선언할거에요 -> 추가 이동 속도인 bonusMoveSpeed를
|
||||||
public float bonusAttackDamage;
|
public float bonusAttackDamage; // 변수를 선언할거에요 -> 추가 공격력인 bonusAttackDamage를
|
||||||
|
|
||||||
[Header("--- 장착 장비 ---")]
|
[Header("--- 장착 장비 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 장착 장비 --- 를
|
||||||
public float weaponDamage;
|
public float weaponDamage; // 변수를 선언할거에요 -> 무기 데미지인 weaponDamage를
|
||||||
|
|
||||||
[Header("--- 밸런스 설정 ---")]
|
[Header("--- 밸런스 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 밸런스 설정 --- 을
|
||||||
[SerializeField] private float runSpeedMultiplier = 1.5f;
|
[SerializeField] private float runSpeedMultiplier = 1.5f; // 변수를 선언할거에요 -> 달리기 속도 배율인 runSpeedMultiplier를
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
* 실제 게임 로직용 프로퍼티
|
* 실제 게임 로직용 프로퍼티
|
||||||
* ========================= */
|
* ========================= */
|
||||||
public float MaxHealth => baseMaxHealth + bonusMaxHealth;
|
public float MaxHealth => baseMaxHealth + bonusMaxHealth; // 프로퍼티를 선언할거에요 -> 총 최대 체력을 반환하는 MaxHealth를
|
||||||
public float BaseAttackDamage => baseAttackDamage + bonusAttackDamage;
|
public float BaseAttackDamage => baseAttackDamage + bonusAttackDamage; // 프로퍼티를 선언할거에요 -> 총 기본 공격력을 반환하는 BaseAttackDamage를
|
||||||
public float TotalAttackDamage => BaseAttackDamage + weaponDamage;
|
public float TotalAttackDamage => BaseAttackDamage + weaponDamage; // 프로퍼티를 선언할거에요 -> 최종 공격력(무기 포함)을 반환하는 TotalAttackDamage를
|
||||||
|
|
||||||
// ✨ [수정] 이제 무게 페널티 없이 순수 속도만 계산합니다.
|
// ✨ [수정] 이제 무게 페널티 없이 순수 속도만 계산합니다.
|
||||||
public float CurrentMoveSpeed => baseMoveSpeed + bonusMoveSpeed;
|
public float CurrentMoveSpeed => baseMoveSpeed + bonusMoveSpeed; // 프로퍼티를 선언할거에요 -> 현재 이동 속도를 반환하는 CurrentMoveSpeed를
|
||||||
public float CurrentRunSpeed => CurrentMoveSpeed * runSpeedMultiplier;
|
public float CurrentRunSpeed => CurrentMoveSpeed * runSpeedMultiplier; // 프로퍼티를 선언할거에요 -> 현재 달리기 속도를 반환하는 CurrentRunSpeed를
|
||||||
|
|
||||||
private void Update()
|
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를
|
||||||
{
|
{
|
||||||
finalMaxHealth = MaxHealth;
|
finalMaxHealth = MaxHealth; // 값을 갱신할거에요 -> 디버그용 최종 체력을
|
||||||
finalMoveSpeed = CurrentMoveSpeed;
|
finalMoveSpeed = CurrentMoveSpeed; // 값을 갱신할거에요 -> 디버그용 최종 속도를
|
||||||
finalAttackDamage = TotalAttackDamage;
|
finalAttackDamage = TotalAttackDamage; // 값을 갱신할거에요 -> 디버그용 최종 공격력을
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✨ [수정] 레벨업 시 체력과 공격력을 올려주도록 변경
|
// ✨ [수정] 레벨업 시 체력과 공격력을 올려주도록 변경
|
||||||
public void AddBaseLevelUpStats(float hpAdd, float dmgAdd)
|
public void AddBaseLevelUpStats(float hpAdd, float dmgAdd) // 함수를 선언할거에요 -> 레벨업 보너스를 적용하는 AddBaseLevelUpStats를
|
||||||
{
|
{
|
||||||
baseMaxHealth += hpAdd;
|
baseMaxHealth += hpAdd; // 값을 더할거에요 -> 기본 체력에 증가분을
|
||||||
baseAttackDamage += dmgAdd;
|
baseAttackDamage += dmgAdd; // 값을 더할거에요 -> 기본 공격력에 증가분을
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddMaxHealth(float value) => bonusMaxHealth += value;
|
public void AddMaxHealth(float value) => bonusMaxHealth += value; // 함수를 선언할거에요 -> 추가 체력을 늘리는 AddMaxHealth를
|
||||||
public void AddMoveSpeed(float value) => bonusMoveSpeed += value;
|
public void AddMoveSpeed(float value) => bonusMoveSpeed += value; // 함수를 선언할거에요 -> 추가 속도를 늘리는 AddMoveSpeed를
|
||||||
public void AddAttackDamage(float value) => bonusAttackDamage += value;
|
public void AddAttackDamage(float value) => bonusAttackDamage += value; // 함수를 선언할거에요 -> 추가 공격력을 늘리는 AddAttackDamage를
|
||||||
|
|
||||||
// ✨ [제거] 무게 및 힘 관련 함수들(UpdateWeaponWeight, ResetWeight)이 삭제되었습니다.
|
// ✨ [제거] 무게 및 힘 관련 함수들(UpdateWeaponWeight, ResetWeight)이 삭제되었습니다.
|
||||||
|
|
||||||
[Header("--- 최종 능력치 (Read Only) ---")]
|
[Header("--- 최종 능력치 (Read Only) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 최종 능력치 (Read Only) --- 를
|
||||||
[SerializeField] private float finalMaxHealth;
|
[SerializeField] private float finalMaxHealth; // 변수를 선언할거에요 -> 디버그용 최종 체력 변수를
|
||||||
[SerializeField] private float finalMoveSpeed;
|
[SerializeField] private float finalMoveSpeed; // 변수를 선언할거에요 -> 디버그용 최종 속도 변수를
|
||||||
[SerializeField] private float finalAttackDamage;
|
[SerializeField] private float finalAttackDamage; // 변수를 선언할거에요 -> 디버그용 최종 공격력 변수를
|
||||||
}
|
}
|
||||||
|
|
@ -1,27 +1,27 @@
|
||||||
using System.Collections;
|
using System.Collections; // 컬렉션 기능을 사용할거에요 -> System.Collections를
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic; // 제네릭 컬렉션 기능을 사용할거에요 -> System.Collections.Generic을
|
||||||
using TMPro;
|
using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를
|
||||||
using Unity.VisualScripting;
|
using Unity.VisualScripting; // 유니티 비주얼 스크립팅 기능을 사용할거에요 -> Unity.VisualScripting을
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
[CreateAssetMenu(menuName = "LevelUp/Card")]
|
[CreateAssetMenu(menuName = "LevelUp/Card")] // 메뉴를 만들거에요 -> 에셋 생성 메뉴에 "LevelUp/Card"를
|
||||||
public abstract class CardData : ScriptableObject
|
public abstract class CardData : ScriptableObject // 추상 클래스를 선언할거에요 -> ScriptableObject를 상속받는 CardData를
|
||||||
{
|
{
|
||||||
public Sprite icon;
|
public Sprite icon; // 변수를 선언할거에요 -> 카드 아이콘 이미지를 icon에
|
||||||
|
|
||||||
// ⭐ 추가: 이 카드가 나타나기 위해 필요한 '복수의 집착' 레벨
|
// ⭐ 추가: 이 카드가 나타나기 위해 필요한 '복수의 집착' 레벨
|
||||||
// 기본값을 1로 설정하면 처음부터 등장합니다.
|
// 기본값을 1로 설정하면 처음부터 등장합니다.
|
||||||
public int requiredObsessionLevel = 1;
|
public int requiredObsessionLevel = 1; // 변수를 선언할거에요 -> 등장 조건 레벨(1)을 requiredObsessionLevel에
|
||||||
|
|
||||||
// UI 표시용
|
// UI 표시용
|
||||||
public abstract string GetText();
|
public abstract string GetText(); // 추상 함수를 선언할거에요 -> 카드 설명 텍스트를 반환하는 GetText를
|
||||||
|
|
||||||
// 외부에서 호출되는 공통 실행 함수
|
// 외부에서 호출되는 공통 실행 함수
|
||||||
public void Execute()
|
public void Execute() // 함수를 선언할거에요 -> 카드 효과를 실행하는 공통 함수 Execute를
|
||||||
{
|
{
|
||||||
ApplyEffect();
|
ApplyEffect(); // 함수를 실행할거에요 -> 실제 효과 적용 함수를
|
||||||
}
|
}
|
||||||
|
|
||||||
// 실제 효과는 자식이 구현
|
// 실제 효과는 자식이 구현
|
||||||
protected abstract void ApplyEffect();
|
protected abstract void ApplyEffect(); // 추상 함수를 선언할거에요 -> 자식 클래스에서 구현할 효과 적용 함수 ApplyEffect를
|
||||||
}
|
}
|
||||||
|
|
@ -1,31 +1,31 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
[CreateAssetMenu(menuName = "LevelUp/RandomStatCard")]
|
[CreateAssetMenu(menuName = "LevelUp/RandomStatCard")] // 메뉴를 만들거에요 -> 에셋 생성 메뉴에 "LevelUp/RandomStatCard"를
|
||||||
public class RandomStatCardData : CardData
|
public class RandomStatCardData : CardData // 클래스를 선언할거에요 -> CardData를 상속받는 RandomStatCardData를
|
||||||
{
|
{
|
||||||
public StatType[] possibleStats;
|
public StatType[] possibleStats; // 배열을 선언할거에요 -> 등장 가능한 스탯 목록을 possibleStats에
|
||||||
public int minValue = 1;
|
public int minValue = 1; // 변수를 선언할거에요 -> 스탯 증가 최소값을 minValue에
|
||||||
public int maxValue = 3;
|
public int maxValue = 3; // 변수를 선언할거에요 -> 스탯 증가 최대값을 maxValue에
|
||||||
|
|
||||||
// ⭐ 기존의 stat1, value1 같은 변수들은 다 지워버리세요!
|
// ⭐ 기존의 stat1, value1 같은 변수들은 다 지워버리세요!
|
||||||
// 이제 CardUI가 기억할 거니까 여기엔 필요 없습니다.
|
// 이제 CardUI가 기억할 거니까 여기엔 필요 없습니다.
|
||||||
|
|
||||||
public override string GetText() => ""; // CardUI에서 직접 만드니까 비워둡니다.
|
public override string GetText() => ""; // 함수를 덮어씌울거에요 -> 텍스트 반환 함수를 (CardUI가 처리하므로 빈 문자열 반환)
|
||||||
|
|
||||||
protected override void ApplyEffect() { } // 사용하지 않음
|
protected override void ApplyEffect() { } // 함수를 덮어씌울거에요 -> 기본 효과 적용 함수를 (여기선 안 씀)
|
||||||
|
|
||||||
// ⭐ CardUI가 호출할 수 있게 public으로 만듭니다.
|
// ⭐ CardUI가 호출할 수 있게 public으로 만듭니다.
|
||||||
public void ApplyToPlayer(StatType stat, int value)
|
public void ApplyToPlayer(StatType stat, int value) // 함수를 선언할거에요 -> 플레이어에게 스탯을 적용하는 ApplyToPlayer를
|
||||||
{
|
{
|
||||||
Stats stats = FindObjectOfType<Stats>();
|
Stats stats = FindObjectOfType<Stats>(); // 컴포넌트를 찾을거에요 -> 씬에 있는 Stats 스크립트를
|
||||||
if (stats == null) return;
|
if (stats == null) return; // 조건이 맞으면 중단할거에요 -> Stats가 없다면
|
||||||
|
|
||||||
switch (stat)
|
switch (stat) // 분기할거에요 -> 스탯 타입(stat)에 따라
|
||||||
{
|
{
|
||||||
case StatType.Health: stats.AddMaxHealth(value); break;
|
case StatType.Health: stats.AddMaxHealth(value); break; // 일치하면 실행할거에요 -> 최대 체력 증가 함수를
|
||||||
case StatType.Speed: stats.AddMoveSpeed(value); break;
|
case StatType.Speed: stats.AddMoveSpeed(value); break; // 일치하면 실행할거에요 -> 이동 속도 증가 함수를
|
||||||
// case StatType.Strength: stats.AddStrength(value); break;
|
// case StatType.Strength: stats.AddStrength(value); break; // (주석 처리됨) 힘 증가 로직
|
||||||
case StatType.Damage: stats.AddAttackDamage(value); break;
|
case StatType.Damage: stats.AddAttackDamage(value); break; // 일치하면 실행할거에요 -> 공격력 증가 함수를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,59 +1,59 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using TMPro;
|
using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를
|
||||||
using UnityEngine.UI; //
|
using UnityEngine.UI; // 유니티 UI 기능을 사용할거에요 -> UnityEngine.UI를
|
||||||
|
|
||||||
public class CardUI : MonoBehaviour
|
public class CardUI : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 CardUI를
|
||||||
{
|
{
|
||||||
[SerializeField] private TextMeshProUGUI effectText;
|
[SerializeField] private TextMeshProUGUI effectText; // 변수를 선언할거에요 -> 효과 설명 텍스트 UI를 effectText에
|
||||||
[SerializeField] private Image iconImage;
|
[SerializeField] private Image iconImage; // 변수를 선언할거에요 -> 아이콘 이미지 UI를 iconImage에
|
||||||
[SerializeField] private Outline selectionOutline; // ⭐ 인스펙터에서 테두리 컴포넌트 연결
|
[SerializeField] private Outline selectionOutline; // 변수를 선언할거에요 -> 선택 시 켜질 테두리 컴포넌트를 selectionOutline에
|
||||||
|
|
||||||
private CardData cardData;
|
private CardData cardData; // 변수를 선언할거에요 -> 이 UI가 표시할 카드 데이터를 cardData에
|
||||||
private LevelUpUIManager uiManager;
|
private LevelUpUIManager uiManager; // 변수를 선언할거에요 -> UI 매니저를 uiManager에
|
||||||
|
|
||||||
private StatType s1, s2;
|
private StatType s1, s2; // 변수를 선언할거에요 -> 랜덤 스탯 타입 2개를 저장할 s1, s2를
|
||||||
private int v1, v2;
|
private int v1, v2; // 변수를 선언할거에요 -> 랜덤 스탯 수치 2개를 저장할 v1, v2를
|
||||||
private bool isRandomCard = false;
|
private bool isRandomCard = false; // 변수를 초기화할거에요 -> 랜덤 카드 여부를 거짓으로
|
||||||
|
|
||||||
public void Setup(CardData data, LevelUpUIManager manager)
|
public void Setup(CardData data, LevelUpUIManager manager) // 함수를 선언할거에요 -> 카드를 설정하는 Setup을
|
||||||
{
|
{
|
||||||
cardData = data;
|
cardData = data; // 값을 저장할거에요 -> 전달받은 카드 데이터를
|
||||||
uiManager = manager;
|
uiManager = manager; // 값을 저장할거에요 -> 전달받은 매니저를
|
||||||
if (selectionOutline != null) selectionOutline.enabled = false; // 처음엔 테두리 끔
|
if (selectionOutline != null) selectionOutline.enabled = false; // 조건이 맞으면 실행할거에요 -> 테두리가 있다면 끄기를
|
||||||
|
|
||||||
// (랜덤 스탯 카드 데이터 처리 로직 - 기존 코드 유지)
|
// (랜덤 스탯 카드 데이터 처리 로직 - 기존 코드 유지)
|
||||||
RandomStatCardData randomData = cardData as RandomStatCardData;
|
RandomStatCardData randomData = cardData as RandomStatCardData; // 형변환을 시도할거에요 -> 카드 데이터를 랜덤 스탯 카드 데이터로
|
||||||
if (randomData != null)
|
if (randomData != null) // 조건이 맞으면 실행할거에요 -> 랜덤 스탯 카드가 맞다면
|
||||||
{
|
{
|
||||||
isRandomCard = true;
|
isRandomCard = true; // 상태를 바꿀거에요 -> 랜덤 카드 상태를 참으로
|
||||||
s1 = randomData.possibleStats[Random.Range(0, randomData.possibleStats.Length)];
|
s1 = randomData.possibleStats[Random.Range(0, randomData.possibleStats.Length)]; // 값을 뽑을거에요 -> 첫 번째 스탯을 랜덤으로
|
||||||
do { s2 = randomData.possibleStats[Random.Range(0, randomData.possibleStats.Length)]; } while (s1 == s2);
|
do { s2 = randomData.possibleStats[Random.Range(0, randomData.possibleStats.Length)]; } while (s1 == s2); // 반복할거에요 -> 두 번째 스탯이 첫 번째와 다를 때까지 뽑기를
|
||||||
v1 = Random.Range(Mathf.Max(1, randomData.minValue), Mathf.Max(1, randomData.maxValue) + 1);
|
v1 = Random.Range(Mathf.Max(1, randomData.minValue), Mathf.Max(1, randomData.maxValue) + 1); // 값을 뽑을거에요 -> 첫 번째 수치를 범위 내 랜덤으로
|
||||||
v2 = Random.Range(Mathf.Min(-1, randomData.minValue), -1 + 1);
|
v2 = Random.Range(Mathf.Min(-1, randomData.minValue), -1 + 1); // 값을 뽑을거에요 -> 두 번째 수치를 (음수 범위 등 고려하여) 랜덤으로
|
||||||
effectText.text = $"{s1} +{v1}\n{s2} {v2}";
|
effectText.text = $"{s1} +{v1}\n{s2} {v2}"; // 텍스트를 설정할거에요 -> 뽑힌 스탯과 수치를 UI에 표시하도록
|
||||||
}
|
}
|
||||||
else { effectText.text = cardData.GetText(); }
|
else { effectText.text = cardData.GetText(); } // 조건이 틀리면 실행할거에요 -> 일반 카드라면 기본 텍스트를 표시하도록
|
||||||
}
|
}
|
||||||
|
|
||||||
// 카드 클릭 시 매니저에게 알림
|
// 카드 클릭 시 매니저에게 알림
|
||||||
public void OnClick() { uiManager.OnCardClick(this); }
|
public void OnClick() { uiManager.OnCardClick(this); } // 함수를 실행할거에요 -> 매니저에게 이 카드가 클릭되었음을 알리기를
|
||||||
|
|
||||||
// 선택 시 하이라이트 연출
|
// 선택 시 하이라이트 연출
|
||||||
public void SetSelected(bool isSelected)
|
public void SetSelected(bool isSelected) // 함수를 선언할거에요 -> 선택 상태를 설정하는 SetSelected를
|
||||||
{
|
{
|
||||||
if (selectionOutline != null) selectionOutline.enabled = isSelected;
|
if (selectionOutline != null) selectionOutline.enabled = isSelected; // 조건이 맞으면 실행할거에요 -> 테두리를 켜거나 끄기를
|
||||||
transform.localScale = isSelected ? Vector3.one * 1.05f : Vector3.one;
|
transform.localScale = isSelected ? Vector3.one * 1.05f : Vector3.one; // 크기를 조절할거에요 -> 선택되면 살짝 키우고 아니면 원래대로
|
||||||
}
|
}
|
||||||
|
|
||||||
// APPLY 버튼 누를 때 최종 실행될 효과
|
// APPLY 버튼 누를 때 최종 실행될 효과
|
||||||
public void ApplyCurrentEffect()
|
public void ApplyCurrentEffect() // 함수를 선언할거에요 -> 효과를 최종 적용하는 ApplyCurrentEffect를
|
||||||
{
|
{
|
||||||
if (isRandomCard)
|
if (isRandomCard) // 조건이 맞으면 실행할거에요 -> 랜덤 카드라면
|
||||||
{
|
{
|
||||||
RandomStatCardData rd = cardData as RandomStatCardData;
|
RandomStatCardData rd = cardData as RandomStatCardData; // 형변환할거에요 -> 데이터를 랜덤 카드 데이터로
|
||||||
rd.ApplyToPlayer(s1, v1); rd.ApplyToPlayer(s2, v2);
|
rd.ApplyToPlayer(s1, v1); rd.ApplyToPlayer(s2, v2); // 실행할거에요 -> 저장해둔 스탯(s1, s2)과 수치(v1, v2)를 플레이어에게 적용하기를
|
||||||
}
|
}
|
||||||
else { cardData.Execute(); }
|
else { cardData.Execute(); } // 조건이 틀리면 실행할거에요 -> 일반 카드라면 기본 실행 함수를
|
||||||
FindObjectOfType<PlayerHealth>()?.RefreshHealthUI();
|
FindObjectOfType<PlayerHealth>()?.RefreshHealthUI(); // 실행할거에요 -> 체력 UI 갱신을 (혹시 체력이 변했을 수 있으니)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,79 +1,79 @@
|
||||||
using System.Collections;
|
using System.Collections; // 컬렉션 기능을 사용할거에요 -> System.Collections를
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic; // 제네릭 컬렉션 기능을 사용할거에요 -> System.Collections.Generic을
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
public class SettingPanelController : MonoBehaviour
|
public class SettingPanelController : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 SettingPanelController를
|
||||||
{
|
{
|
||||||
[SerializeField] private GameObject settingPanel;
|
[SerializeField] private GameObject settingPanel; // 변수를 선언할거에요 -> 설정 패널 오브젝트인 settingPanel을
|
||||||
[SerializeField] private GameObject gameStopPanel;
|
[SerializeField] private GameObject gameStopPanel; // 변수를 선언할거에요 -> 일시정지 패널 오브젝트인 gameStopPanel을
|
||||||
[HideInInspector] public bool IsSettingOpen { get; private set; }
|
[HideInInspector] public bool IsSettingOpen { get; private set; } // 프로퍼티를 선언할거에요 -> 설정창 열림 여부를 나타내는 IsSettingOpen을 (인스펙터에서는 숨김)
|
||||||
|
|
||||||
[Header("설정창 선택 패널")]
|
[Header("설정창 선택 패널")] // 인스펙터 헤더를 작성할거에요 -> "설정창 선택 패널"이라는 제목을
|
||||||
[SerializeField] private GameObject normalPanel;
|
[SerializeField] private GameObject normalPanel; // 변수를 선언할거에요 -> 일반 설정 패널인 normalPanel을
|
||||||
[SerializeField] private GameObject graphicPanel;
|
[SerializeField] private GameObject graphicPanel; // 변수를 선언할거에요 -> 그래픽 설정 패널인 graphicPanel을
|
||||||
|
|
||||||
[Header("열고 닫는 소리")]
|
[Header("열고 닫는 소리")] // 인스펙터 헤더를 작성할거에요 -> "열고 닫는 소리"라는 제목을
|
||||||
[SerializeField] private AudioSource audioSource;
|
[SerializeField] private AudioSource audioSource; // 변수를 선언할거에요 -> 소리를 재생할 오디오 소스 컴포넌트를
|
||||||
[SerializeField] private AudioClip openSound;
|
[SerializeField] private AudioClip openSound; // 변수를 선언할거에요 -> 열릴 때 재생할 오디오 클립을
|
||||||
[SerializeField] private AudioClip closeSound;
|
[SerializeField] private AudioClip closeSound; // 변수를 선언할거에요 -> 닫힐 때 재생할 오디오 클립을
|
||||||
|
|
||||||
public void AllClose()
|
public void AllClose() // 함수를 선언할거에요 -> 모든 하위 패널을 닫는 AllClose를
|
||||||
{
|
{
|
||||||
normalPanel.SetActive(false);
|
normalPanel.SetActive(false); // 기능을 껄거에요 -> 일반 설정 패널을
|
||||||
graphicPanel.SetActive(false);
|
graphicPanel.SetActive(false); // 기능을 껄거에요 -> 그래픽 설정 패널을
|
||||||
}
|
}
|
||||||
|
|
||||||
public void NormalOpen()
|
public void NormalOpen() // 함수를 선언할거에요 -> 일반 설정 패널을 여는 NormalOpen을
|
||||||
{
|
{
|
||||||
AllClose();
|
AllClose(); // 함수를 실행할거에요 -> 모든 패널을 닫는 기능을
|
||||||
normalPanel.SetActive(true);
|
normalPanel.SetActive(true); // 기능을 켤거에요 -> 일반 설정 패널을
|
||||||
if (audioSource != null && openSound != null)
|
if (audioSource != null && openSound != null) // 조건이 맞으면 실행할거에요 -> 오디오 소스와 열기 사운드가 있다면
|
||||||
{
|
{
|
||||||
audioSource.PlayOneShot(openSound);
|
audioSource.PlayOneShot(openSound); // 소리를 재생할거에요 -> 열기 효과음을 한 번
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void GraphicOpen()
|
public void GraphicOpen() // 함수를 선언할거에요 -> 그래픽 설정 패널을 여는 GraphicOpen을
|
||||||
{
|
{
|
||||||
AllClose();
|
AllClose(); // 함수를 실행할거에요 -> 모든 패널을 닫는 기능을
|
||||||
graphicPanel.SetActive(true);
|
graphicPanel.SetActive(true); // 기능을 켤거에요 -> 그래픽 설정 패널을
|
||||||
if (audioSource != null && openSound != null)
|
if (audioSource != null && openSound != null) // 조건이 맞으면 실행할거에요 -> 오디오 소스와 열기 사운드가 있다면
|
||||||
{
|
{
|
||||||
audioSource.PlayOneShot(openSound);
|
audioSource.PlayOneShot(openSound); // 소리를 재생할거에요 -> 열기 효과음을 한 번
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OpenSetting()
|
public void OpenSetting() // 함수를 선언할거에요 -> 설정창 전체를 여는 OpenSetting을
|
||||||
{
|
{
|
||||||
IsSettingOpen = true;
|
IsSettingOpen = true; // 상태를 바꿀거에요 -> 설정창 열림 상태를 참(true)으로
|
||||||
|
|
||||||
if (audioSource != null && openSound != null)
|
if (audioSource != null && openSound != null) // 조건이 맞으면 실행할거에요 -> 오디오 소스와 열기 사운드가 있다면
|
||||||
{
|
{
|
||||||
audioSource.PlayOneShot(openSound);
|
audioSource.PlayOneShot(openSound); // 소리를 재생할거에요 -> 열기 효과음을 한 번
|
||||||
}
|
}
|
||||||
|
|
||||||
settingPanel.SetActive(true);
|
settingPanel.SetActive(true); // 기능을 켤거에요 -> 설정 패널 전체를
|
||||||
|
|
||||||
if (gameStopPanel != null)
|
if (gameStopPanel != null) // 조건이 맞으면 실행할거에요 -> 일시정지 패널이 연결되어 있다면
|
||||||
{
|
{
|
||||||
gameStopPanel.SetActive(false);
|
gameStopPanel.SetActive(false); // 기능을 껄거에요 -> 일시정지 패널을 (설정창이랑 겹치지 않게)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CloseSetting()
|
public void CloseSetting() // 함수를 선언할거에요 -> 설정창을 닫는 CloseSetting을
|
||||||
{
|
{
|
||||||
IsSettingOpen = false;
|
IsSettingOpen = false; // 상태를 바꿀거에요 -> 설정창 열림 상태를 거짓(false)으로
|
||||||
|
|
||||||
if (audioSource != null && closeSound != null)
|
if (audioSource != null && closeSound != null) // 조건이 맞으면 실행할거에요 -> 오디오 소스와 닫기 사운드가 있다면
|
||||||
{
|
{
|
||||||
audioSource.PlayOneShot(closeSound);
|
audioSource.PlayOneShot(closeSound); // 소리를 재생할거에요 -> 닫기 효과음을 한 번
|
||||||
}
|
}
|
||||||
|
|
||||||
settingPanel.SetActive(false);
|
settingPanel.SetActive(false); // 기능을 껄거에요 -> 설정 패널 전체를
|
||||||
|
|
||||||
if (gameStopPanel != null)
|
if (gameStopPanel != null) // 조건이 맞으면 실행할거에요 -> 일시정지 패널이 연결되어 있다면
|
||||||
{
|
{
|
||||||
gameStopPanel.SetActive(true);
|
gameStopPanel.SetActive(true); // 기능을 켤거에요 -> 일시정지 패널을 다시 보이게
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,50 +1,50 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic; // 리스트와 딕셔너리 기능을 사용할거에요 -> System.Collections.Generic을
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
public class GenericObjectPool : MonoBehaviour
|
public class GenericObjectPool : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 GenericObjectPool을
|
||||||
{
|
{
|
||||||
public static GenericObjectPool Instance;
|
public static GenericObjectPool Instance; // 변수를 선언할거에요 -> 싱글톤 인스턴스인 Instance를
|
||||||
|
|
||||||
[System.Serializable]
|
[System.Serializable] // 직렬화할거에요 -> 인스펙터에서 수정 가능하게 Pool 클래스를
|
||||||
public class Pool
|
public class Pool // 내부 클래스를 선언할거에요 -> 풀 정보를 담을 Pool을
|
||||||
{
|
{
|
||||||
public string tag;
|
public string tag; // 변수를 선언할거에요 -> 풀의 태그 이름을 tag에
|
||||||
public GameObject prefab;
|
public GameObject prefab; // 변수를 선언할거에요 -> 생성할 프리팹을 prefab에
|
||||||
public int size;
|
public int size; // 변수를 선언할거에요 -> 미리 생성할 개수를 size에
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Pool> pools;
|
public List<Pool> pools; // 리스트를 선언할거에요 -> 설정된 풀 목록을 pools에
|
||||||
public Dictionary<string, Queue<GameObject>> poolDictionary;
|
public Dictionary<string, Queue<GameObject>> poolDictionary; // 딕셔너리를 선언할거에요 -> 태그별 오브젝트 큐를 관리할 poolDictionary를
|
||||||
|
|
||||||
private void Awake()
|
private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를
|
||||||
{
|
{
|
||||||
Instance = this;
|
Instance = this; // 값을 저장할거에요 -> 내 자신을 싱글톤 인스턴스에
|
||||||
poolDictionary = new Dictionary<string, Queue<GameObject>>();
|
poolDictionary = new Dictionary<string, Queue<GameObject>>(); // 초기화할거에요 -> 딕셔너리를 새로 생성해서
|
||||||
|
|
||||||
foreach (Pool pool in pools)
|
foreach (Pool pool in pools) // 반복할거에요 -> 설정된 모든 풀 정보에 대해
|
||||||
{
|
{
|
||||||
Queue<GameObject> objectPool = new Queue<GameObject>();
|
Queue<GameObject> objectPool = new Queue<GameObject>(); // 큐를 생성할거에요 -> 오브젝트를 담을 큐를
|
||||||
for (int i = 0; i < pool.size; i++)
|
for (int i = 0; i < pool.size; i++) // 반복할거에요 -> 설정된 사이즈만큼
|
||||||
{
|
{
|
||||||
GameObject obj = Instantiate(pool.prefab);
|
GameObject obj = Instantiate(pool.prefab); // 생성할거에요 -> 프리팹을 인스턴스로
|
||||||
obj.SetActive(false); // 꺼둔 상태로 보관
|
obj.SetActive(false); // 기능을 껄거에요 -> 생성된 오브젝트를 비활성화 상태로 (보관)
|
||||||
objectPool.Enqueue(obj);
|
objectPool.Enqueue(obj); // 추가할거에요 -> 큐에 오브젝트를
|
||||||
}
|
}
|
||||||
poolDictionary.Add(pool.tag, objectPool);
|
poolDictionary.Add(pool.tag, objectPool); // 추가할거에요 -> 딕셔너리에 태그와 큐를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⭐ 몹을 꺼내 쓸 때 호출 (Instantiate 대신 사용)
|
// ⭐ 몹을 꺼내 쓸 때 호출 (Instantiate 대신 사용)
|
||||||
public GameObject SpawnFromPool(string tag, Vector3 position, Quaternion rotation)
|
public GameObject SpawnFromPool(string tag, Vector3 position, Quaternion rotation) // 함수를 선언할거에요 -> 풀에서 오브젝트를 꺼내는 SpawnFromPool을
|
||||||
{
|
{
|
||||||
if (!poolDictionary.ContainsKey(tag)) return null;
|
if (!poolDictionary.ContainsKey(tag)) return null; // 조건이 맞으면 반환할거에요 -> 태그가 없다면 null을
|
||||||
|
|
||||||
GameObject objectToSpawn = poolDictionary[tag].Dequeue();
|
GameObject objectToSpawn = poolDictionary[tag].Dequeue(); // 꺼낼거에요 -> 큐의 맨 앞 오브젝트를
|
||||||
objectToSpawn.SetActive(true);
|
objectToSpawn.SetActive(true); // 기능을 켤거에요 -> 오브젝트를 활성화해서
|
||||||
objectToSpawn.transform.position = position;
|
objectToSpawn.transform.position = position; // 위치를 설정할거에요 -> 지정된 위치로
|
||||||
objectToSpawn.transform.rotation = rotation;
|
objectToSpawn.transform.rotation = rotation; // 회전을 설정할거에요 -> 지정된 회전으로
|
||||||
|
|
||||||
poolDictionary[tag].Enqueue(objectToSpawn); // 다시 큐의 끝으로 보냄
|
poolDictionary[tag].Enqueue(objectToSpawn); // 추가할거에요 -> 다시 큐의 맨 뒤로 (재사용 대기)
|
||||||
return objectToSpawn;
|
return objectToSpawn; // 반환할거에요 -> 꺼낸 오브젝트를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,73 +1,73 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using UnityEditor;
|
using UnityEditor; // 유니티 에디터 기능을 사용할거에요 -> UnityEditor를
|
||||||
|
|
||||||
namespace GameSystems.Optimization.Editor
|
namespace GameSystems.Optimization.Editor // 에디터 네임스페이스를 정의할거에요 -> GameSystems.Optimization.Editor로
|
||||||
{
|
{
|
||||||
[CustomEditor(typeof(PlayerRangeManager))]
|
[CustomEditor(typeof(PlayerRangeManager))] // 커스텀 에디터 대상을 지정할거에요 -> PlayerRangeManager 클래스로
|
||||||
public sealed class PlayerRangeManagerEditor : UnityEditor.Editor
|
public sealed class PlayerRangeManagerEditor : UnityEditor.Editor // 클래스를 선언할거에요 -> 에디터 상속을 받는 PlayerRangeManagerEditor를
|
||||||
{
|
{
|
||||||
private SerializedProperty _configProperty;
|
private SerializedProperty _configProperty; // 변수를 선언할거에요 -> 설정 속성을 연결할 _configProperty를
|
||||||
private SerializedProperty _parentsProperty; // ⭐ 변수명 일치
|
private SerializedProperty _parentsProperty; // 변수를 선언할거에요 -> 부모 목록 속성을 연결할 _parentsProperty를
|
||||||
|
|
||||||
private void OnEnable()
|
private void OnEnable() // 에디터가 활성화될 때 실행할거에요 -> OnEnable 함수를
|
||||||
{
|
{
|
||||||
_configProperty = serializedObject.FindProperty("_config");
|
_configProperty = serializedObject.FindProperty("_config"); // 찾을거에요 -> 대상의 "_config" 변수를
|
||||||
// ⭐ [에러 해결] 리스트 이름 '_environmentParents'를 정확히 찾아옵니다.
|
// ⭐ [에러 해결] 리스트 이름 '_environmentParents'를 정확히 찾아옵니다.
|
||||||
_parentsProperty = serializedObject.FindProperty("_environmentParents");
|
_parentsProperty = serializedObject.FindProperty("_environmentParents"); // 찾을거에요 -> 대상의 "_environmentParents" 변수를
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnInspectorGUI()
|
public override void OnInspectorGUI() // 인스펙터 화면을 그릴거에요 -> OnInspectorGUI 함수를
|
||||||
{
|
{
|
||||||
PlayerRangeManager manager = (PlayerRangeManager)target;
|
PlayerRangeManager manager = (PlayerRangeManager)target; // 변수를 설정할거에요 -> 현재 선택된 매니저 객체로
|
||||||
serializedObject.Update();
|
serializedObject.Update(); // 실행할거에요 -> 객체의 최신 데이터를 불러오기를
|
||||||
|
|
||||||
// === 헤더 ===
|
// === 헤더 ===
|
||||||
DrawMainHeader(); // ⭐ [경고 해결] DrawHeader 이름을 DrawMainHeader로 변경
|
DrawMainHeader(); // 함수를 실행할거에요 -> 메인 헤더를 그리는
|
||||||
|
|
||||||
EditorGUILayout.Space(5);
|
EditorGUILayout.Space(5); // 공간을 줄거에요 -> 5픽셀만큼의 여백을
|
||||||
|
|
||||||
// === 설정 영역 ===
|
// === 설정 영역 ===
|
||||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox); // 수직 영역을 시작할거에요 -> 헬프박스 스타일로
|
||||||
GUILayout.Label("필수 설정", EditorStyles.boldLabel);
|
GUILayout.Label("필수 설정", EditorStyles.boldLabel); // 라벨을 표시할거에요 -> "필수 설정"이라는 제목을
|
||||||
EditorGUILayout.PropertyField(_configProperty);
|
EditorGUILayout.PropertyField(_configProperty); // 그려낼거에요 -> 설정 에셋 입력 필드를
|
||||||
|
|
||||||
// ⭐ [에러 해결] 리스트를 안전하게 그립니다.
|
// ⭐ [에러 해결] 리스트를 안전하게 그립니다.
|
||||||
if (_parentsProperty != null)
|
if (_parentsProperty != null) // 조건이 맞으면 실행할거에요 -> 부모 속성이 유효하다면
|
||||||
{
|
{
|
||||||
EditorGUILayout.PropertyField(_parentsProperty, new GUIContent("Environment Parents"), true);
|
EditorGUILayout.PropertyField(_parentsProperty, new GUIContent("Environment Parents"), true); // 그려낼거에요 -> 자식 요소까지 포함한 리스트 필드를
|
||||||
}
|
}
|
||||||
|
|
||||||
EditorGUILayout.EndVertical();
|
EditorGUILayout.EndVertical(); // 수직 영역을 종료할거에요
|
||||||
|
|
||||||
// === 실시간 통계 ===
|
// === 실시간 통계 ===
|
||||||
if (Application.isPlaying && manager.IsInitialized)
|
if (Application.isPlaying && manager.IsInitialized) // 조건이 맞으면 실행할거에요 -> 게임 중이고 초기화되었다면
|
||||||
{
|
{
|
||||||
EditorGUILayout.Space(5);
|
EditorGUILayout.Space(5); // 여백을 줄거에요
|
||||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox); // 수직 영역을 시작할거에요
|
||||||
GUILayout.Label("실시간 통계", EditorStyles.boldLabel);
|
GUILayout.Label("실시간 통계", EditorStyles.boldLabel); // 제목을 표시할거에요 -> "실시간 통계"를
|
||||||
EditorGUILayout.LabelField("총 오브젝트:", manager.TotalObjectCount.ToString());
|
EditorGUILayout.LabelField("총 오브젝트:", manager.TotalObjectCount.ToString()); // 텍스트를 표시할거에요 -> 총 개수 정보를
|
||||||
EditorGUILayout.LabelField("표시 중:", manager.VisibleObjectCount.ToString());
|
EditorGUILayout.LabelField("표시 중:", manager.VisibleObjectCount.ToString()); // 텍스트를 표시할거에요 -> 현재 표시 개수 정보를
|
||||||
EditorGUILayout.LabelField("컬링 효율:", $"{(manager.CullingEfficiency * 100f):F1}%");
|
EditorGUILayout.LabelField("컬링 효율:", $"{(manager.CullingEfficiency * 100f):F1}%"); // 텍스트를 표시할거에요 -> 효율 퍼센트 정보를
|
||||||
EditorGUILayout.EndVertical();
|
EditorGUILayout.EndVertical(); // 수직 영역을 종료할거에요
|
||||||
}
|
}
|
||||||
|
|
||||||
EditorGUILayout.Space(5);
|
EditorGUILayout.Space(5); // 여백을 줄거에요
|
||||||
|
|
||||||
// === 컨트롤 버튼 ===
|
// === 컨트롤 버튼 ===
|
||||||
if (GUILayout.Button("🔄 오브젝트 목록 새로고침", GUILayout.Height(30)))
|
if (GUILayout.Button("🔄 오브젝트 목록 새로고침", GUILayout.Height(30))) // 조건이 맞으면 실행할거에요 -> 버튼을 눌렀다면
|
||||||
{
|
{
|
||||||
if (Application.isPlaying) manager.RefreshObjectList();
|
if (Application.isPlaying) manager.RefreshObjectList(); // 실행할거에요 -> 실행 중일 때 목록 갱신 기능을
|
||||||
}
|
}
|
||||||
|
|
||||||
serializedObject.ApplyModifiedProperties();
|
serializedObject.ApplyModifiedProperties(); // 실행할거에요 -> 변경된 데이터를 실제 객체에 반영하기를
|
||||||
if (Application.isPlaying) Repaint();
|
if (Application.isPlaying) Repaint(); // 실행할거에요 -> 실행 중일 때 화면을 다시 그리기를
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawMainHeader() // ⭐ 이름 변경됨
|
private void DrawMainHeader() // 함수를 선언할거에요 -> 메인 헤더를 그리는 DrawMainHeader를
|
||||||
{
|
{
|
||||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox); // 수직 영역을 시작할거에요
|
||||||
GUILayout.Label("Player Range Manager", new GUIStyle(EditorStyles.boldLabel) { fontSize = 14, alignment = TextAnchor.MiddleCenter });
|
GUILayout.Label("Player Range Manager", new GUIStyle(EditorStyles.boldLabel) { fontSize = 14, alignment = TextAnchor.MiddleCenter }); // 큰 제목을 그릴거에요 -> 중앙 정렬된 스타일로
|
||||||
EditorGUILayout.EndVertical();
|
EditorGUILayout.EndVertical(); // 수직 영역을 종료할거에요
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,77 +1,68 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
|
||||||
|
|
||||||
public class PlayerRangeManager : MonoBehaviour
|
public class PlayerRangeManager : MonoBehaviour // 클래스를 선언할거에요 -> 거리 기반 최적화 컴포넌트인 PlayerRangeManager를
|
||||||
{
|
{
|
||||||
[Header("--- 대상 설정 ---")]
|
[Header("--- 대상 설정 ---")] // 제목을 표시할거에요 -> --- 대상 설정 --- 을
|
||||||
[SerializeField] private Transform environmentParent; // 맵 오브젝트 부모 폴더
|
[SerializeField] private Transform environmentParent; // 변수를 선언할거에요 -> 맵 오브젝트들이 있는 부모 폴더를
|
||||||
|
|
||||||
[Header("--- 최적화 설정 ---")]
|
[Header("--- 최적화 설정 ---")] // 제목을 표시할거에요 -> --- 최적화 설정 --- 을
|
||||||
[SerializeField] private float renderRange = 40f; // ⭐ 반드시 30~50 사이로 키워주세요!
|
[SerializeField] private float renderRange = 40f; // 변수를 선언할거에요 -> 표시 사거리인 40을 renderRange에
|
||||||
[SerializeField] private float checkInterval = 0.2f;
|
[SerializeField] private float checkInterval = 0.2f; // 변수를 선언할거에요 -> 검사 주기인 0.2를 checkInterval에
|
||||||
|
|
||||||
private struct RenderGroup
|
private struct RenderGroup // 구조체를 정의할거에요 -> 렌더링 그룹 데이터 구조를
|
||||||
{
|
{
|
||||||
public Vector3 actualCenter; // ⭐ 피벗이 아닌 실제 메쉬의 중심점
|
public Vector3 actualCenter; // 변수를 선언할거에요 -> 실제 메쉬의 중심 위치를
|
||||||
public Renderer[] renderers;
|
public Renderer[] renderers; // 배열을 선언할거에요 -> 포함된 렌더러 목록을
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<RenderGroup> _childGroups = new List<RenderGroup>();
|
private List<RenderGroup> _childGroups = new List<RenderGroup>(); // 리스트를 생성할거에요 -> 하위 그룹들을 관리할 목록을
|
||||||
private Transform _player;
|
private Transform _player; // 변수를 선언할거에요 -> 플레이어 위치 정보를 담을 변수를
|
||||||
|
|
||||||
private void Start()
|
private void Start() // 함수를 실행할거에요 -> 시작 시 Start를
|
||||||
{
|
{
|
||||||
// 태그로 플레이어를 정확히 찾습니다.
|
// 태그로 플레이어를 정확히 찾습니다.
|
||||||
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
|
GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); // 찾을거에요 -> "Player" 태그를 가진 오브젝트를
|
||||||
if (playerObj != null) _player = playerObj.transform;
|
if (playerObj != null) _player = playerObj.transform; // 조건이 맞으면 저장할거에요 -> 플레이어 위치를
|
||||||
else _player = transform;
|
else _player = transform; // 조건이 틀리면 저장할거에요 -> 내 위치를 대신
|
||||||
|
|
||||||
if (environmentParent != null) RefreshChildList();
|
if (environmentParent != null) RefreshChildList(); // 조건이 맞으면 실행할거에요 -> 환경 오브젝트 목록 갱신을
|
||||||
InvokeRepeating(nameof(UpdateCulling), 0f, checkInterval);
|
InvokeRepeating(nameof(UpdateCulling), 0f, checkInterval); // 반복 실행할거에요 -> 컬링 업데이트 함수를 주기적으로
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RefreshChildList()
|
public void RefreshChildList() // 함수를 선언할거에요 -> 자식 목록을 새로고침하는 RefreshChildList를
|
||||||
{
|
{
|
||||||
if (environmentParent == null) return;
|
if (environmentParent == null) return; // 조건이 맞으면 중단할거에요 -> 부모 폴더가 없다면
|
||||||
_childGroups.Clear();
|
_childGroups.Clear(); // 리스트를 비울거에요 -> 기존 목록을
|
||||||
|
|
||||||
foreach (Transform child in environmentParent)
|
foreach (Transform child in environmentParent) // 반복할거에요 -> 부모 내의 모든 자식 오브젝트에 대해
|
||||||
{
|
{
|
||||||
Renderer[] rs = child.GetComponentsInChildren<Renderer>(true);
|
Renderer[] rs = child.GetComponentsInChildren<Renderer>(true); // 배열을 가져올거에요 -> 자식 내부의 모든 렌더러들을
|
||||||
if (rs.Length > 0)
|
if (rs.Length > 0) // 조건이 맞으면 실행할거에요 -> 렌더러가 존재한다면
|
||||||
{
|
{
|
||||||
// ⭐ [수정] 피벗(position)이 아니라, 실제 모델링의 중심(bounds.center)을 가져옵니다.
|
// ⭐ [수정] 피벗(position)이 아니라, 실제 모델링의 중심(bounds.center)을 가져옵니다.
|
||||||
// 이래야 "가까이 갔는데 꺼지는" 현상이 완벽히 해결됩니다!
|
// 이래야 "가까이 갔는데 꺼지는" 현상이 완벽히 해결됩니다!
|
||||||
Vector3 center = rs[0].bounds.center;
|
Vector3 center = rs[0].bounds.center; // 위치를 가져올거에요 -> 첫 번째 렌더러의 실제 경계 중심점을
|
||||||
_childGroups.Add(new RenderGroup { actualCenter = center, renderers = rs });
|
_childGroups.Add(new RenderGroup { actualCenter = center, renderers = rs }); // 리스트에 추가할거에요 -> 새로운 그룹 정보를 생성해서
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateCulling()
|
private void UpdateCulling() // 함수를 선언할거에요 -> 실제 컬링 로직인 UpdateCulling을
|
||||||
{
|
{
|
||||||
if (_player == null) return;
|
if (_player == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어가 없다면
|
||||||
Vector3 playerPos = _player.position;
|
Vector3 playerPos = _player.position; // 값을 가져올거에요 -> 현재 플레이어의 위치를
|
||||||
|
|
||||||
foreach (var group in _childGroups)
|
foreach (var group in _childGroups) // 반복할거에요 -> 모든 관리 그룹에 대해
|
||||||
{
|
{
|
||||||
// ⭐ [핵심] 실제 몸통 중심점과의 거리를 계산합니다.
|
// ⭐ [핵심] 실제 몸통 중심점과의 거리를 계산합니다.
|
||||||
float dist = Vector3.Distance(playerPos, group.actualCenter);
|
float dist = Vector3.Distance(playerPos, group.actualCenter); // 거리를 계산할거에요 -> 플레이어와 그룹 중심 사이의
|
||||||
bool shouldShow = dist <= renderRange;
|
bool shouldShow = dist <= renderRange; // 상태를 결정할거에요 -> 범위 이내인지 여부를
|
||||||
|
|
||||||
foreach (Renderer r in group.renderers)
|
foreach (Renderer r in group.renderers) // 반복할거에요 -> 그룹 내 모든 렌더러에 대해
|
||||||
{
|
{
|
||||||
if (r != null && r.enabled != shouldShow) r.enabled = shouldShow;
|
if (r != null && r.enabled != shouldShow) r.enabled = shouldShow; // 조건이 맞으면 상태를 바꿀거에요 -> 렌더러의 활성화 여부를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDrawGizmos()
|
|
||||||
{
|
|
||||||
if (_player != null)
|
|
||||||
{
|
|
||||||
Gizmos.color = Color.red;
|
|
||||||
Gizmos.DrawWireSphere(_player.position, renderRange); // 플레이어를 따라다니는 빨간 원
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,151 +1,157 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
|
||||||
|
|
||||||
namespace GameSystems.Optimization
|
namespace GameSystems.Optimization // 최적화 네임스페이스를 정의할거에요 -> GameSystems.Optimization으로
|
||||||
{
|
{
|
||||||
[DisallowMultipleComponent]
|
[DisallowMultipleComponent] // 기능을 제한할거에요 -> 중복 부착을 방지하도록
|
||||||
[AddComponentMenu("Game Systems/Optimization/Player Range Manager")]
|
[AddComponentMenu("Game Systems/Optimization/Player Range Manager")] // 메뉴를 추가할거에요 -> 컴포넌트 추가 경로에
|
||||||
public sealed class PlayerRangeManager : MonoBehaviour
|
public sealed class PlayerRangeManager : MonoBehaviour // 클래스를 선언할거에요 -> 최적화 관리자인 PlayerRangeManager를
|
||||||
{
|
{
|
||||||
#region Serialized Fields
|
#region Serialized Fields
|
||||||
|
|
||||||
[Header("=== 필수 설정 ===")]
|
[Header("=== 필수 설정 ===")] // 제목을 표시할거에요 -> === 필수 설정 === 을
|
||||||
[SerializeField, Tooltip("최적화 설정 에셋")]
|
[SerializeField, Tooltip("최적화 설정 에셋")] // 필드를 직렬화할거에요 -> 인스펙터 노출을 위해
|
||||||
private RenderOptimizationConfig _config;
|
private RenderOptimizationConfig _config; // 변수를 선언할거에요 -> 설정 에셋을 담을 _config를
|
||||||
|
|
||||||
[SerializeField, Tooltip("환경 오브젝트들이 들어있는 부모 폴더 리스트")]
|
[SerializeField, Tooltip("환경 오브젝트들이 들어있는 부모 폴더 리스트")] // 툴팁을 추가할거에요 -> 설명 문구를
|
||||||
private List<Transform> _environmentParents = new List<Transform>(); // ⭐ 리스트 방식으로 변경
|
private List<Transform> _environmentParents = new List<Transform>(); // 리스트를 생성할거에요 -> 부모 폴더들을 관리할 목록을
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Private Fields
|
#region Private Fields
|
||||||
|
|
||||||
private Transform _playerTransform;
|
private Transform _playerTransform; // 변수를 선언할거에요 -> 플레이어의 위치 정보를 저장할 변수를
|
||||||
private readonly List<RenderGroup> _renderGroups = new List<RenderGroup>(512);
|
private readonly List<RenderGroup> _renderGroups = new List<RenderGroup>(512); // 리스트를 생성할거에요 -> 렌더 그룹들을 담을 목록을
|
||||||
|
|
||||||
private bool _isInitialized;
|
private bool _isInitialized; // 변수를 선언할거에요 -> 초기화 완료 여부를
|
||||||
private bool _isPaused;
|
private bool _isPaused; // 변수를 선언할거에요 -> 시스템 일시정지 여부를
|
||||||
|
|
||||||
private int _totalObjectCount;
|
private int _totalObjectCount; // 변수를 선언할거에요 -> 관리 대상 총 개수를
|
||||||
private int _visibleObjectCount;
|
private int _visibleObjectCount; // 변수를 선언할거에요 -> 화면에 표시 중인 개수를
|
||||||
private int _lastFrameChangedCount;
|
private int _lastFrameChangedCount; // 변수를 선언할거에요 -> 지난 프레임의 상태 변화 수를
|
||||||
private float _nextCheckTime;
|
private float _nextCheckTime; // 변수를 선언할거에요 -> 다음 검사 시간을
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Public Properties
|
#region Public Properties
|
||||||
|
|
||||||
public bool IsInitialized => _isInitialized;
|
public bool IsInitialized => _isInitialized; // 값을 반환할거에요 -> 초기화 완료 여부 상태를
|
||||||
public bool IsPaused => _isPaused;
|
public bool IsPaused => _isPaused; // 값을 반환할거에요 -> 일시정지 여부 상태를
|
||||||
public int TotalObjectCount => _totalObjectCount;
|
public int TotalObjectCount => _totalObjectCount; // 값을 반환할거에요 -> 총 관리 개수 수치를
|
||||||
public int VisibleObjectCount => _visibleObjectCount;
|
public int VisibleObjectCount => _visibleObjectCount; // 값을 반환할거에요 -> 현재 표시 개수 수치를
|
||||||
public float CullingEfficiency => _totalObjectCount > 0 ? 1f - ((float)_visibleObjectCount / _totalObjectCount) : 0f;
|
public float CullingEfficiency => _totalObjectCount > 0 ? 1f - ((float)_visibleObjectCount / _totalObjectCount) : 0f; // 값을 계산해서 반환할거에요 -> 컬링 효율 비율을
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private void Awake() { ValidateConfiguration(); }
|
private void Awake() { ValidateConfiguration(); } // 함수를 실행할거에요 -> 시작 시 설정을 검증하는 Awake를
|
||||||
private void Start() { InitializeSystem(); }
|
private void Start() { InitializeSystem(); } // 함수를 실행할거에요 -> 시스템을 초기화하는 Start를
|
||||||
|
|
||||||
private void Update()
|
private void Update() // 함수를 실행할거에요 -> 매 프레임 업데이트 로직인 Update를
|
||||||
{
|
{
|
||||||
if (!_isInitialized || _isPaused) return;
|
if (!_isInitialized || _isPaused || _playerTransform == null) return; // 조건이 맞으면 중단할거에요 -> 동작 불가능한 상태라면
|
||||||
if (Time.time >= _nextCheckTime)
|
|
||||||
|
if (Time.time >= _nextCheckTime) // 조건이 맞으면 실행할거에요 -> 다음 검사 시간이 되었다면
|
||||||
{
|
{
|
||||||
UpdateCulling();
|
UpdateCulling(); // 함수를 실행할거에요 -> 컬링 업데이트 기능을
|
||||||
_nextCheckTime = Time.time + GetCheckInterval();
|
_nextCheckTime = Time.time + GetCheckInterval(); // 값을 갱신할거에요 -> 다음 검사 예정 시간을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ValidateConfiguration()
|
private void ValidateConfiguration() // 함수를 선언할거에요 -> 설정 누락 여부를 확인하는 ValidateConfiguration을
|
||||||
{
|
{
|
||||||
if (_config == null) _config = RenderOptimizationConfig.CreateDefault();
|
if (_config == null) // 조건이 맞으면 실행할거에요 -> 설정 에셋이 비어있다면
|
||||||
}
|
|
||||||
|
|
||||||
public void InitializeSystem()
|
|
||||||
{
|
{
|
||||||
if (!FindPlayer()) return;
|
Debug.LogWarning("[PlayerRangeManager] Config가 없습니다! 기본값을 생성합니다."); // 경고를 출력할거에요 -> 자동 생성 메시지를
|
||||||
if (!ScanEnvironmentObjects()) return;
|
_config = RenderOptimizationConfig.CreateDefault(); // 값을 설정할거에요 -> 기본값으로 생성된 에셋을
|
||||||
|
}
|
||||||
_isInitialized = true;
|
|
||||||
_nextCheckTime = Time.time;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool FindPlayer()
|
private void InitializeSystem() // 함수를 선언할거에요 -> 전체 시스템을 초기화하는 InitializeSystem을
|
||||||
{
|
{
|
||||||
GameObject playerObj = GameObject.FindGameObjectWithTag(GetPlayerTag());
|
FindPlayer(); // 함수를 실행할거에요 -> 플레이어를 찾는 기능을
|
||||||
_playerTransform = (playerObj != null) ? playerObj.transform : transform;
|
ScanEnvironmentObjects(); // 함수를 실행할거에요 -> 주변 오브젝트를 스캔하는 기능을
|
||||||
return true;
|
|
||||||
|
_isInitialized = true; // 상태를 바꿀거에요 -> 초기화 완료인 참(true)으로
|
||||||
|
if (ShouldLog()) Debug.Log($"[PlayerRangeManager] 초기화 완료. 대상 그룹: {_totalObjectCount}"); // 조건이 맞으면 로그를 출력할거에요 -> 결과 보고 메시지를
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⭐ [핵심 수정] 리스트 내의 모든 폴더를 뒤져서 '낱개' 단위로 등록합니다.
|
private void FindPlayer() // 함수를 선언할거에요 -> 플레이어를 탐색하는 FindPlayer를
|
||||||
private bool ScanEnvironmentObjects()
|
|
||||||
{
|
{
|
||||||
if (_environmentParents == null || _environmentParents.Count == 0) return false;
|
GameObject player = GameObject.FindGameObjectWithTag(GetPlayerTag()); // 오브젝트를 찾을거에요 -> 지정된 태그를 가진 플레이어를
|
||||||
|
if (player != null) // 조건이 맞으면 실행할거에요 -> 플레이어를 찾았다면
|
||||||
_renderGroups.Clear();
|
|
||||||
|
|
||||||
foreach (Transform parent in _environmentParents)
|
|
||||||
{
|
{
|
||||||
if (parent == null) continue;
|
_playerTransform = player.transform; // 값을 저장할거에요 -> 플레이어의 Transform 정보를
|
||||||
|
}
|
||||||
// ⭐ 폴더 구조가 몇 층이든 상관없이 모든 Renderer를 낱낱이 찾아냅니다.
|
else // 조건이 틀리면 실행할거에요 -> 플레이어를 못 찾았다면
|
||||||
Renderer[] allRenderers = parent.GetComponentsInChildren<Renderer>(_config.IncludeInactiveObjects);
|
|
||||||
|
|
||||||
foreach (Renderer r in allRenderers)
|
|
||||||
{
|
{
|
||||||
if (r == null) continue;
|
Debug.LogError($"[PlayerRangeManager] '{GetPlayerTag()}' 태그의 플레이어를 찾을 수 없습니다!"); // 에러를 출력할거에요 -> 태그 불일치 메시지를
|
||||||
// 각 렌더러를 독립적인 그룹으로 생성 (개별 최적화의 핵심)
|
|
||||||
_renderGroups.Add(new RenderGroup(new Renderer[] { r }));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_totalObjectCount = _renderGroups.Count;
|
private void ScanEnvironmentObjects() // 함수를 선언할거에요 -> 환경 오브젝트들을 스캔하는 ScanEnvironmentObjects를
|
||||||
return _totalObjectCount > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateCulling()
|
|
||||||
{
|
{
|
||||||
if (_playerTransform == null) return;
|
_renderGroups.Clear(); // 리스트를 비울거에요 -> 기존에 있던 렌더 그룹들을
|
||||||
|
|
||||||
Vector3 playerPosition = _playerTransform.position;
|
foreach (var parent in _environmentParents) // 반복할거에요 -> 등록된 모든 부모 폴더에 대해
|
||||||
float maxDistance = GetRenderRange();
|
|
||||||
int changedCount = 0;
|
|
||||||
int visibleCount = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < _renderGroups.Count; i++)
|
|
||||||
{
|
{
|
||||||
if (_renderGroups[i].UpdateVisibility(playerPosition, maxDistance)) changedCount++;
|
if (parent == null) continue; // 조건이 맞으면 건너뛸거에요 -> 부모가 비어있다면
|
||||||
if (_renderGroups[i].IsVisible) visibleCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
_visibleObjectCount = visibleCount;
|
foreach (Transform child in parent) // 반복할거에요 -> 부모 폴더의 모든 자식 오브젝트에 대해
|
||||||
_lastFrameChangedCount = changedCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RefreshObjectList() { ScanEnvironmentObjects(); }
|
|
||||||
public void ShowAllObjects() { foreach (var g in _renderGroups) g.ForceShow(); _visibleObjectCount = _totalObjectCount; }
|
|
||||||
public void HideAllObjects() { foreach (var g in _renderGroups) g.ForceHide(); _visibleObjectCount = 0; }
|
|
||||||
public void SetPaused(bool paused) => _isPaused = paused;
|
|
||||||
|
|
||||||
private float GetRenderRange() => _config.RenderRange;
|
|
||||||
private float GetCheckInterval() => _config.CheckInterval;
|
|
||||||
private string GetPlayerTag() => _config.PlayerTag;
|
|
||||||
private bool ShouldShowGizmos() => _config.ShowGizmos;
|
|
||||||
|
|
||||||
private void OnDrawGizmos()
|
|
||||||
{
|
{
|
||||||
if (!ShouldShowGizmos() || _playerTransform == null) return;
|
Renderer[] renderers = child.GetComponentsInChildren<Renderer>(true); // 배열을 가져올거에요 -> 자식 내부의 모든 렌더러들을
|
||||||
Gizmos.color = new Color(1f, 0f, 0f, 0.3f);
|
if (renderers.Length > 0) // 조건이 맞으면 실행할거에요 -> 렌더러가 존재한다면
|
||||||
Gizmos.DrawWireSphere(_playerTransform.position, GetRenderRange());
|
{
|
||||||
|
_renderGroups.Add(new RenderGroup(renderers)); // 리스트에 추가할거에요 -> 새로운 렌더 그룹을 생성해서
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDrawGizmosSelected()
|
_totalObjectCount = _renderGroups.Count; // 값을 저장할거에요 -> 총 관리 대상 개수를
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCulling() // 함수를 선언할거에요 -> 컬링을 업데이트하는 UpdateCulling을
|
||||||
{
|
{
|
||||||
if (!ShouldShowGizmos() || !_isInitialized) return;
|
Vector3 playerPos = _playerTransform.position; // 값을 가져올거에요 -> 현재 플레이어의 위치를
|
||||||
foreach (var group in _renderGroups)
|
float range = GetRenderRange(); // 값을 가져올거에요 -> 설정된 렌더링 거리 수치를
|
||||||
|
|
||||||
|
int changedCount = 0; // 변수를 초기화할거에요 -> 변경 개수를 0으로
|
||||||
|
int visibleCount = 0; // 변수를 초기화할거에요 -> 표시 중인 개수를 0으로
|
||||||
|
|
||||||
|
for (int i = 0; i < _renderGroups.Count; i++) // 반복할거에요 -> 모든 렌더 그룹에 대해
|
||||||
{
|
{
|
||||||
Gizmos.color = group.IsVisible ? new Color(0f, 1f, 0f, 0.5f) : new Color(0.5f, 0.5f, 0.5f, 0.2f);
|
if (_renderGroups[i].UpdateVisibility(playerPos, range)) changedCount++; // 조건이 맞으면 증가시킬거에요 -> 상태가 바뀌었다면 변경 수를
|
||||||
Gizmos.DrawSphere(group.ActualCenter, 0.3f);
|
if (_renderGroups[i].IsVisible) visibleCount++; // 조건이 맞으면 증가시킬거에요 -> 현재 보이고 있다면 표시 수를
|
||||||
|
}
|
||||||
|
|
||||||
|
_visibleObjectCount = visibleCount; // 값을 저장할거에요 -> 이번 검사 결과의 총 표시 개수를
|
||||||
|
_lastFrameChangedCount = changedCount; // 값을 저장할거에요 -> 변화가 발생한 그룹 수를
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshObjectList() { ScanEnvironmentObjects(); } // 함수를 실행할거에요 -> 목록을 새로 스캔하는 기능을
|
||||||
|
public void ShowAllObjects() { foreach (var g in _renderGroups) g.ForceShow(); _visibleObjectCount = _totalObjectCount; } // 함수를 실행할거에요 -> 모든 대상을 켜는 기능을
|
||||||
|
public void HideAllObjects() { foreach (var g in _renderGroups) g.ForceHide(); _visibleObjectCount = 0; } // 함수를 실행할거에요 -> 모든 대상을 끄는 기능을
|
||||||
|
public void SetPaused(bool paused) => _isPaused = paused; // 값을 바꿀거에요 -> 일시정지 여부를 전달받은 인자대로
|
||||||
|
|
||||||
|
private float GetRenderRange() => _config.RenderRange; // 값을 가져올거에요 -> 설정 파일의 렌더링 사거리 수치를
|
||||||
|
private float GetCheckInterval() => _config.CheckInterval; // 값을 가져올거에요 -> 설정 파일의 검사 간격 수치를
|
||||||
|
private string GetPlayerTag() => _config.PlayerTag; // 값을 가져올거에요 -> 설정 파일의 플레이어 태그를
|
||||||
|
private bool ShouldShowGizmos() => _config.ShowGizmos; // 값을 가져올거에요 -> 설정 파일의 기즈모 표시 여부를
|
||||||
|
private bool ShouldLog() => _config.EnableVerboseLogging; // 값을 가져올거에요 -> 설정 파일의 로그 활성화 여부를
|
||||||
|
|
||||||
|
private void OnDrawGizmos() // 함수를 실행할거에요 -> 기즈모를 그리는 OnDrawGizmos를
|
||||||
|
{
|
||||||
|
if (!ShouldShowGizmos() || _playerTransform == null) return; // 조건이 맞으면 중단할거에요 -> 표시 설정이 꺼졌거나 플레이어가 없다면
|
||||||
|
Gizmos.color = new Color(1f, 0f, 0f, 0.3f); // 색상을 설정할거에요 -> 빨간색 반투명으로
|
||||||
|
Gizmos.DrawWireSphere(_playerTransform.position, GetRenderRange()); // 그림을 그릴거에요 -> 플레이어 중심의 사거리 구체를
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 선택 시 기즈모를 그리는 OnDrawGizmosSelected를
|
||||||
|
{
|
||||||
|
if (!ShouldShowGizmos() || !_isInitialized) return; // 조건이 맞으면 중단할거에요 -> 표시 설정이 꺼졌거나 초기화 전이라면
|
||||||
|
foreach (var group in _renderGroups) // 반복할거에요 -> 모든 렌더 그룹에 대해
|
||||||
|
{
|
||||||
|
Gizmos.color = group.IsVisible ? new Color(0f, 1f, 0f, 0.5f) : new Color(0.5f, 0.5f, 0.5f, 0.2f); // 색상을 결정할거에요 -> 상태에 따라 초록 혹은 회색으로
|
||||||
|
Gizmos.DrawWireSphere(group.ActualCenter, group.BoundingRadius); // 그림을 그릴거에요 -> 각 그룹의 경계면 구체를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
namespace GameSystems.Optimization
|
namespace GameSystems.Optimization // 최적화 네임스페이스를 정의할거에요 -> GameSystems.Optimization으로
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 렌더링 최적화를 위한 오브젝트 그룹 데이터
|
/// 렌더링 최적화를 위한 오브젝트 그룹 데이터
|
||||||
/// - 불변성(Immutability) 보장
|
/// - 불변성(Immutability) 보장
|
||||||
/// - 캡슐화된 내부 데이터
|
/// - 캡슐화된 내부 데이터
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class RenderGroup
|
public sealed class RenderGroup // 클래스를 선언할거에요 -> 상속 불가능한 최적화 그룹인 RenderGroup을
|
||||||
{
|
{
|
||||||
#region Private Fields
|
#region Private Fields
|
||||||
|
|
||||||
private readonly Vector3 _actualCenter;
|
private readonly Vector3 _actualCenter; // 변수를 선언할거에요 -> 읽기 전용으로 실제 중심 좌표를
|
||||||
private readonly Renderer[] _renderers;
|
private readonly Renderer[] _renderers; // 배열을 선언할거에요 -> 읽기 전용으로 렌더러 목록을
|
||||||
private readonly float _boundingRadius;
|
private readonly float _boundingRadius; // 변수를 선언할거에요 -> 읽기 전용으로 경계 반지름 수치를
|
||||||
private bool _isCurrentlyVisible;
|
private bool _isCurrentlyVisible; // 변수를 선언할거에요 -> 현재 표시 상태를 저장할 변수를
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
@ -23,22 +23,22 @@ namespace GameSystems.Optimization
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 실제 메쉬의 중심점 (피벗이 아닌 bounds 기준)
|
/// 실제 메쉬의 중심점 (피벗이 아닌 bounds 기준)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Vector3 ActualCenter => _actualCenter;
|
public Vector3 ActualCenter => _actualCenter; // 프로퍼티를 선언할거에요 -> 실제 중심점을 반환하는 기능을
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 오브젝트의 최대 반지름 (경계 구체)
|
/// 오브젝트의 최대 반지름 (경계 구체)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float BoundingRadius => _boundingRadius;
|
public float BoundingRadius => _boundingRadius; // 프로퍼티를 선언할거에요 -> 경계 반지름을 반환하는 기능을
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 현재 렌더링 상태
|
/// 현재 렌더링 상태
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsVisible => _isCurrentlyVisible;
|
public bool IsVisible => _isCurrentlyVisible; // 프로퍼티를 선언할거에요 -> 표시 여부 상태를 반환하는 기능을
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 관리 중인 Renderer 개수
|
/// 관리 중인 Renderer 개수
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int RendererCount => _renderers?.Length ?? 0;
|
public int RendererCount => _renderers?.Length ?? 0; // 프로퍼티를 선언할거에요 -> 렌더러 개수를 반환하는 기능을 (null이면 0)
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
@ -48,28 +48,28 @@ namespace GameSystems.Optimization
|
||||||
/// RenderGroup 생성자
|
/// RenderGroup 생성자
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="renderers">대상 Renderer 배열 (null 체크 수행)</param>
|
/// <param name="renderers">대상 Renderer 배열 (null 체크 수행)</param>
|
||||||
public RenderGroup(Renderer[] renderers)
|
public RenderGroup(Renderer[] renderers) // 생성자를 실행할거에요 -> 렌더러 배열을 인자로 받아서
|
||||||
{
|
{
|
||||||
if (renderers == null || renderers.Length == 0)
|
if (renderers == null || renderers.Length == 0) // 조건이 맞으면 실행할거에요 -> 렌더러가 비어있거나 없다면
|
||||||
{
|
{
|
||||||
Debug.LogError("[RenderGroup] Renderer 배열이 null 또는 비어있습니다.");
|
Debug.LogError("[RenderGroup] Renderer 배열이 null 또는 비어있습니다."); // 에러를 출력할거에요 -> 로그 메시지를
|
||||||
_renderers = System.Array.Empty<Renderer>();
|
_renderers = System.Array.Empty<Renderer>(); // 값을 할당할거에요 -> 빈 렌더러 배열을
|
||||||
_actualCenter = Vector3.zero;
|
_actualCenter = Vector3.zero; // 값을 할당할거에요 -> 제로 벡터를 중심점으로
|
||||||
_boundingRadius = 0f;
|
_boundingRadius = 0f; // 값을 할당할거에요 -> 0을 반지름으로
|
||||||
return;
|
return; // 중단할거에요 -> 초기화 로직을
|
||||||
}
|
}
|
||||||
|
|
||||||
// 방어적 복사 (외부에서 배열 수정 방지)
|
// 방어적 복사 (외부에서 배열 수정 방지)
|
||||||
_renderers = new Renderer[renderers.Length];
|
_renderers = new Renderer[renderers.Length]; // 배열을 생성할거에요 -> 같은 크기의 새로운 렌더러 배열을
|
||||||
System.Array.Copy(renderers, _renderers, renderers.Length);
|
System.Array.Copy(renderers, _renderers, renderers.Length); // 값을 복사할거에요 -> 원본 렌더러들을 내부 배열로
|
||||||
|
|
||||||
// 실제 중심점 계산
|
// 실제 중심점 계산
|
||||||
_actualCenter = CalculateGroupCenter(_renderers);
|
_actualCenter = CalculateGroupCenter(_renderers); // 값을 계산해서 넣을거에요 -> 그룹의 통합 중심점을
|
||||||
|
|
||||||
// 경계 반지름 계산
|
// 경계 반지름 계산
|
||||||
_boundingRadius = CalculateBoundingRadius(_renderers, _actualCenter);
|
_boundingRadius = CalculateBoundingRadius(_renderers, _actualCenter); // 값을 계산해서 넣을거에요 -> 최대 경계 반지름을
|
||||||
|
|
||||||
_isCurrentlyVisible = true; // 초기 상태는 표시
|
_isCurrentlyVisible = true; // 초기값을 설정할거에요 -> 표시 상태를 참(true)으로
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
@ -82,58 +82,58 @@ namespace GameSystems.Optimization
|
||||||
/// <param name="referencePosition">기준 위치 (플레이어 등)</param>
|
/// <param name="referencePosition">기준 위치 (플레이어 등)</param>
|
||||||
/// <param name="maxDistance">최대 렌더링 거리</param>
|
/// <param name="maxDistance">최대 렌더링 거리</param>
|
||||||
/// <returns>상태가 변경되었는지 여부</returns>
|
/// <returns>상태가 변경되었는지 여부</returns>
|
||||||
public bool UpdateVisibility(Vector3 referencePosition, float maxDistance)
|
public bool UpdateVisibility(Vector3 referencePosition, float maxDistance) // 함수를 선언할거에요 -> 거리에 따른 표시 갱신 기능을
|
||||||
{
|
{
|
||||||
// 거리 계산 (오브젝트 크기 고려)
|
// 거리 계산 (오브젝트 크기 고려)
|
||||||
float distance = Vector3.Distance(referencePosition, _actualCenter);
|
float distance = Vector3.Distance(referencePosition, _actualCenter); // 거리를 계산할거에요 -> 기준점과 그룹 중심 사이의
|
||||||
bool shouldBeVisible = (distance - _boundingRadius) <= maxDistance;
|
bool shouldBeVisible = (distance - _boundingRadius) <= maxDistance; // 상태를 판단할거에요 -> 사거리와 반지름을 고려해서 표시 여부를
|
||||||
|
|
||||||
// 상태 변경이 필요한 경우에만 처리
|
// 상태 변경이 필요한 경우에만 처리
|
||||||
if (shouldBeVisible == _isCurrentlyVisible)
|
if (shouldBeVisible == _isCurrentlyVisible) // 조건이 맞으면 실행할거에요 -> 이전 상태와 동일하다면
|
||||||
{
|
{
|
||||||
return false; // 변경 없음
|
return false; // 반환할거에요 -> 변경되지 않았음을
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renderer 상태 일괄 변경
|
// Renderer 상태 일괄 변경
|
||||||
SetRenderersState(shouldBeVisible);
|
SetRenderersState(shouldBeVisible); // 함수를 실행할거에요 -> 모든 렌더러의 활성화 상태를 바꾸는 기능을
|
||||||
_isCurrentlyVisible = shouldBeVisible;
|
_isCurrentlyVisible = shouldBeVisible; // 값을 갱신할거에요 -> 현재 표시 상태를
|
||||||
|
|
||||||
return true; // 변경됨
|
return true; // 반환할거에요 -> 상태가 변경되었음을
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 강제로 모든 Renderer 표시
|
/// 강제로 모든 Renderer 표시
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ForceShow()
|
public void ForceShow() // 함수를 선언할거에요 -> 강제 표시 기능인 ForceShow를
|
||||||
{
|
{
|
||||||
SetRenderersState(true);
|
SetRenderersState(true); // 함수를 실행할거에요 -> 모든 렌더러를 켜는 기능을
|
||||||
_isCurrentlyVisible = true;
|
_isCurrentlyVisible = true; // 상태를 설정할거에요 -> 표시 중인 것으로
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 강제로 모든 Renderer 숨김
|
/// 강제로 모든 Renderer 숨김
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ForceHide()
|
public void ForceHide() // 함수를 선언할거에요 -> 강제 숨김 기능인 ForceHide를
|
||||||
{
|
{
|
||||||
SetRenderersState(false);
|
SetRenderersState(false); // 함수를 실행할거에요 -> 모든 렌더러를 끄는 기능을
|
||||||
_isCurrentlyVisible = false;
|
_isCurrentlyVisible = false; // 상태를 설정할거에요 -> 숨김 중인 것으로
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 그룹 유효성 검사 (Renderer가 파괴되었는지 확인)
|
/// 그룹 유효성 검사 (Renderer가 파괴되었는지 확인)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsValid()
|
public bool IsValid() // 함수를 선언할거에요 -> 유효성 검사 기능인 IsValid를
|
||||||
{
|
{
|
||||||
if (_renderers == null || _renderers.Length == 0)
|
if (_renderers == null || _renderers.Length == 0) // 조건이 맞으면 반환할거에요 -> 배열이 비어있다면 거짓(false)을
|
||||||
return false;
|
return false; // 반환할거에요 -> 유효하지 않음을
|
||||||
|
|
||||||
// 하나라도 유효한 Renderer가 있으면 true
|
// 하나라도 유효한 Renderer가 있으면 true
|
||||||
foreach (Renderer r in _renderers)
|
foreach (Renderer r in _renderers) // 반복할거에요 -> 배열 내 모든 렌더러에 대해
|
||||||
{
|
{
|
||||||
if (r != null) return true;
|
if (r != null) return true; // 조건이 맞으면 반환할거에요 -> 살아있는 객체가 있다면 참(true)을
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false; // 반환할거에요 -> 모두 파괴되었다면 거짓을
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
@ -143,73 +143,73 @@ namespace GameSystems.Optimization
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 여러 Renderer의 통합 중심점 계산
|
/// 여러 Renderer의 통합 중심점 계산
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static Vector3 CalculateGroupCenter(Renderer[] renderers)
|
private static Vector3 CalculateGroupCenter(Renderer[] renderers) // 함수를 선언할거에요 -> 중심점을 계산하는 정적 기능을
|
||||||
{
|
{
|
||||||
if (renderers.Length == 1 && renderers[0] != null)
|
if (renderers.Length == 1 && renderers[0] != null) // 조건이 맞으면 실행할거에요 -> 렌더러가 하나뿐이라면
|
||||||
{
|
{
|
||||||
return renderers[0].bounds.center;
|
return renderers[0].bounds.center; // 반환할거에요 -> 그 렌더러의 중심점을
|
||||||
}
|
}
|
||||||
|
|
||||||
// 모든 bounds를 포함하는 통합 경계 계산
|
// 모든 bounds를 포함하는 통합 경계 계산
|
||||||
Bounds? combinedBounds = null;
|
Bounds? combinedBounds = null; // 변수를 선언할거에요 -> 널 허용 경계값 변수를
|
||||||
|
|
||||||
foreach (Renderer r in renderers)
|
foreach (Renderer r in renderers) // 반복할거에요 -> 모든 렌더러에 대해
|
||||||
{
|
{
|
||||||
if (r == null) continue;
|
if (r == null) continue; // 조건이 맞으면 건너뛸거에요 -> 렌더러가 비어있다면
|
||||||
|
|
||||||
if (!combinedBounds.HasValue)
|
if (!combinedBounds.HasValue) // 조건이 맞으면 실행할거에요 -> 첫 번째 유효한 경계값이라면
|
||||||
{
|
{
|
||||||
combinedBounds = r.bounds;
|
combinedBounds = r.bounds; // 값을 할당할거에요 -> 현재 렌더러의 경계값으로
|
||||||
}
|
}
|
||||||
else
|
else // 조건이 틀리면 실행할거에요 -> 이미 경계값이 존재한다면
|
||||||
{
|
{
|
||||||
Bounds temp = combinedBounds.Value;
|
Bounds temp = combinedBounds.Value; // 변수를 설정할거에요 -> 임시 경계값으로
|
||||||
temp.Encapsulate(r.bounds);
|
temp.Encapsulate(r.bounds); // 영역을 확장할거에요 -> 새로운 렌더러를 포함하도록
|
||||||
combinedBounds = temp;
|
combinedBounds = temp; // 값을 갱신할거에요 -> 확장된 경계값으로
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return combinedBounds?.center ?? Vector3.zero;
|
return combinedBounds?.center ?? Vector3.zero; // 반환할거에요 -> 계산된 중심점을 (없으면 제로 벡터)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 오브젝트의 최대 반지름 계산
|
/// 오브젝트의 최대 반지름 계산
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static float CalculateBoundingRadius(Renderer[] renderers, Vector3 center)
|
private static float CalculateBoundingRadius(Renderer[] renderers, Vector3 center) // 함수를 선언할거에요 -> 반지름을 계산하는 정적 기능을
|
||||||
{
|
{
|
||||||
float maxRadius = 0f;
|
float maxRadius = 0f; // 변수를 초기화할거에요 -> 최대 반지름 수치를 0으로
|
||||||
|
|
||||||
foreach (Renderer r in renderers)
|
foreach (Renderer r in renderers) // 반복할거에요 -> 모든 렌더러에 대해
|
||||||
{
|
{
|
||||||
if (r == null) continue;
|
if (r == null) continue; // 조건이 맞으면 건너뛸거에요 -> 렌더러가 비어있다면
|
||||||
|
|
||||||
// 각 Renderer 중심에서의 거리 + extent 크기
|
// 각 Renderer 중심에서의 거리 + extent 크기
|
||||||
float distanceFromCenter = Vector3.Distance(r.bounds.center, center);
|
float distanceFromCenter = Vector3.Distance(r.bounds.center, center); // 거리를 계산할거에요 -> 렌더러 중심과 그룹 중심 사이의
|
||||||
float extent = r.bounds.extents.magnitude;
|
float extent = r.bounds.extents.magnitude; // 크기를 가져올거에요 -> 경계 상자의 대각선 길이를
|
||||||
float totalRadius = distanceFromCenter + extent;
|
float totalRadius = distanceFromCenter + extent; // 값을 합산할거에요 -> 거리와 크기를 더해서
|
||||||
|
|
||||||
if (totalRadius > maxRadius)
|
if (totalRadius > maxRadius) // 조건이 맞으면 실행할거에요 -> 현재 합계가 더 크다면
|
||||||
{
|
{
|
||||||
maxRadius = totalRadius;
|
maxRadius = totalRadius; // 값을 갱신할거에요 -> 새로운 최대 반지름으로
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return maxRadius;
|
return maxRadius; // 반환할거에요 -> 최종 계산된 최대 반지름을
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 모든 Renderer의 enabled 상태 일괄 변경
|
/// 모든 Renderer의 enabled 상태 일괄 변경
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void SetRenderersState(bool enabled)
|
private void SetRenderersState(bool enabled) // 함수를 선언할거에요 -> 렌더러 상태를 일괄 변경하는 기능을
|
||||||
{
|
{
|
||||||
for (int i = 0; i < _renderers.Length; i++)
|
for (int i = 0; i < _renderers.Length; i++) // 반복할거에요 -> 배열의 인덱스만큼
|
||||||
{
|
{
|
||||||
Renderer r = _renderers[i];
|
Renderer r = _renderers[i]; // 객체를 가져올거에요 -> i번째 렌더러를
|
||||||
|
|
||||||
// null 체크 + 상태가 다를 때만 변경 (성능 최적화)
|
// null 체크 + 상태가 다를 때만 변경 (성능 최적화)
|
||||||
if (r != null && r.enabled != enabled)
|
if (r != null && r.enabled != enabled) // 조건이 맞으면 실행할거에요 -> 렌더러가 존재하고 상태 변경이 필요하다면
|
||||||
{
|
{
|
||||||
r.enabled = enabled;
|
r.enabled = enabled; // 설정을 바꿀거에요 -> 활성화 여부를 인자값대로
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -221,10 +221,10 @@ namespace GameSystems.Optimization
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 리소스 정리 (명시적 호출용)
|
/// 리소스 정리 (명시적 호출용)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Dispose()
|
public void Dispose() // 함수를 선언할거에요 -> 자원 정리 기능을
|
||||||
{
|
{
|
||||||
// Renderer 배열 초기화 (GC 도움)
|
// Renderer 배열 초기화 (GC 도움)
|
||||||
System.Array.Clear(_renderers, 0, _renderers.Length);
|
System.Array.Clear(_renderers, 0, _renderers.Length); // 비울거에요 -> 렌더러 배열의 모든 요소를
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
|
||||||
|
|
@ -1,94 +1,81 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
namespace GameSystems.Optimization
|
namespace GameSystems.Optimization // 최적화 네임스페이스를 정의할거에요 -> GameSystems.Optimization으로
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// PlayerRangeManager의 설정을 관리하는 ScriptableObject
|
/// PlayerRangeManager의 설정을 관리하는 ScriptableObject
|
||||||
/// - 프로젝트 전역 설정 공유
|
|
||||||
/// - 런타임 수정 불가 (데이터 무결성 보장)
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[CreateAssetMenu(fileName = "RenderOptimizationConfig", menuName = "Game Systems/Render Optimization Config")]
|
[CreateAssetMenu(fileName = "RenderOptimizationConfig", menuName = "Game Systems/Render Optimization Config")] // 에셋 생성 메뉴를 만들거에요 -> 지정된 경로에
|
||||||
public sealed class RenderOptimizationConfig : ScriptableObject
|
public sealed class RenderOptimizationConfig : ScriptableObject // 클래스를 선언할거에요 -> ScriptableObject를 상속받는 RenderOptimizationConfig를
|
||||||
{
|
{
|
||||||
#region Inspector Fields
|
#region Inspector Fields
|
||||||
|
|
||||||
[Header("=== 렌더링 범위 설정 ===")]
|
[Header("=== 렌더링 범위 설정 ===")] // 인스펙터에 제목을 표시할거에요 -> === 렌더링 범위 설정 === 을
|
||||||
[Tooltip("플레이어 주변 렌더링 반지름 (유니티 단위)")]
|
[Tooltip("플레이어 주변 렌더링 반지름 (유니티 단위)")] // 툴팁을 추가할거에요 -> 설명 문구를
|
||||||
[SerializeField, Range(10f, 200f)]
|
[SerializeField, Range(10f, 200f)] // 슬라이더 범위를 지정할거에요 -> 10에서 200 사이로
|
||||||
private float _renderRange = 40f;
|
private float _renderRange = 40f; // 변수를 선언할거에요 -> 기본 렌더링 범위인 40을 _renderRange에
|
||||||
|
|
||||||
[Tooltip("페이드 아웃 시작 거리 비율 (0.8 = 렌더 범위의 80%부터 페이드 시작)")]
|
[Tooltip("페이드 아웃 시작 거리 비율 (0.8 = 렌더 범위의 80%부터 페이드 시작)")] // 툴팁을 추가할거에요 -> 비율 설명을
|
||||||
[SerializeField, Range(0.5f, 1f)]
|
[SerializeField, Range(0.5f, 1f)] // 슬라이더 범위를 지정할거에요 -> 0.5에서 1 사이로
|
||||||
private float _fadeStartRatio = 0.8f;
|
private float _fadeStartRatio = 0.8f; // 변수를 선언할거에요 -> 페이드 시작 비율인 0.8을 _fadeStartRatio에
|
||||||
|
|
||||||
[Header("=== 업데이트 주기 ===")]
|
[Header("=== 업데이트 주기 ===")] // 제목을 표시할거에요 -> === 업데이트 주기 === 를
|
||||||
[Tooltip("오브젝트 컬링 검사 주기 (초 단위)")]
|
[Tooltip("오브젝트 컬링 검사 주기 (초 단위)")] // 툴팁을 추가할거에요 -> 검사 주기 설명을
|
||||||
[SerializeField, Range(0.05f, 1f)]
|
[SerializeField, Range(0.05f, 1f)] // 슬라이더 범위를 지정할거에요 -> 0.05에서 1 사이로
|
||||||
private float _checkInterval = 0.2f;
|
private float _checkInterval = 0.2f; // 변수를 선언할거에요 -> 검사 간격인 0.2를 _checkInterval에
|
||||||
|
|
||||||
[Header("=== 플레이어 설정 ===")]
|
[Header("=== 플레이어 설정 ===")] // 제목을 표시할거에요 -> === 플레이어 설정 === 을
|
||||||
[Tooltip("플레이어를 찾을 태그 이름")]
|
[Tooltip("플레이어를 찾을 태그 이름")] // 툴팁을 추가할거에요 -> 태그 이름 설명을
|
||||||
[SerializeField]
|
[SerializeField] // 필드를 직렬화할거에요 -> 인스펙터에 보이게
|
||||||
private string _playerTag = "Player";
|
private string _playerTag = "Player"; // 변수를 선언할거에요 -> 플레이어 태그인 "Player"를 _playerTag에
|
||||||
|
|
||||||
[Header("=== 성능 설정 ===")]
|
[Header("=== 성능 설정 ===")] // 제목을 표시할거에요 -> === 성능 설정 === 을
|
||||||
[Tooltip("한 프레임에 처리할 최대 오브젝트 수 (0 = 무제한)")]
|
[Tooltip("한 프레임에 처리할 최대 오브젝트 수 (0 = 무제한)")] // 툴팁을 추가할거에요 -> 처리량 설명을
|
||||||
[SerializeField, Range(0, 500)]
|
[SerializeField, Min(0)] // 최소값을 지정할거에요 -> 0으로
|
||||||
private int _maxObjectsPerFrame = 0;
|
private int _maxObjectsPerFrame = 50; // 변수를 선언할거에요 -> 프레임당 최대 개수인 50을 _maxObjectsPerFrame에
|
||||||
|
|
||||||
[Tooltip("비활성 오브젝트도 스캔할지 여부")]
|
[Header("=== 디버그 설정 ===")] // 제목을 표시할거에요 -> === 디버그 설정 === 을
|
||||||
[SerializeField]
|
[SerializeField] private bool _showGizmos = true; // 변수를 선언할거에요 -> 기즈모 표시 여부를 _showGizmos에
|
||||||
private bool _includeInactiveObjects = true;
|
[SerializeField] private bool _enableVerboseLogging = false; // 변수를 선언할거에요 -> 상세 로그 여부를 _enableVerboseLogging에
|
||||||
|
|
||||||
[Header("=== 디버그 설정 ===")]
|
|
||||||
[Tooltip("기즈모 표시 여부")]
|
|
||||||
[SerializeField]
|
|
||||||
private bool _showGizmos = true;
|
|
||||||
|
|
||||||
[Tooltip("상세 로그 출력")]
|
|
||||||
[SerializeField]
|
|
||||||
private bool _enableVerboseLogging = false;
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Public Properties (Immutable)
|
#region Public Properties
|
||||||
|
|
||||||
public float RenderRange => _renderRange;
|
public float RenderRange => _renderRange; // 값을 반환할거에요 -> 렌더링 범위 수치를
|
||||||
public float FadeStartRatio => _fadeStartRatio;
|
public float CheckInterval => _checkInterval; // 값을 반환할거에요 -> 검사 주기 수치를
|
||||||
public float CheckInterval => _checkInterval;
|
public string PlayerTag => _playerTag; // 값을 반환할거에요 -> 플레이어 태그 문자열을
|
||||||
public string PlayerTag => _playerTag;
|
public int MaxObjectsPerFrame => _maxObjectsPerFrame; // 값을 반환할거에요 -> 최대 처리 개수를
|
||||||
public int MaxObjectsPerFrame => _maxObjectsPerFrame;
|
public bool ShowGizmos => _showGizmos; // 값을 반환할거에요 -> 기즈모 활성화 여부를
|
||||||
public bool IncludeInactiveObjects => _includeInactiveObjects;
|
public bool EnableVerboseLogging => _enableVerboseLogging; // 값을 반환할거에요 -> 로그 활성화 여부를
|
||||||
public bool ShowGizmos => _showGizmos;
|
|
||||||
public bool EnableVerboseLogging => _enableVerboseLogging;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 페이드 시작 거리 계산
|
/// 페이드 시작 거리 계산
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float FadeStartDistance => _renderRange * _fadeStartRatio;
|
public float FadeStartDistance => _renderRange * _fadeStartRatio; // 값을 계산해서 반환할거에요 -> 렌더링 범위에 비율을 곱한 결과를
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Validation
|
#region Validation
|
||||||
|
|
||||||
private void OnValidate()
|
private void OnValidate() // 값이 변경될 때 실행할거에요 -> OnValidate 함수를
|
||||||
{
|
{
|
||||||
// 유효성 검사
|
// 유효성 검사
|
||||||
if (_renderRange < 10f)
|
if (_renderRange < 10f) // 조건이 맞으면 실행할거에요 -> 렌더링 범위가 10보다 작다면
|
||||||
{
|
{
|
||||||
Debug.LogWarning("[RenderOptimizationConfig] Render Range가 너무 작습니다. 최소 10으로 설정됩니다.");
|
Debug.LogWarning("[RenderOptimizationConfig] Render Range가 너무 작습니다. 최소 10으로 설정됩니다."); // 경고를 출력할거에요 -> 범위 오류 메시지를
|
||||||
_renderRange = 10f;
|
_renderRange = 10f; // 값을 설정할거에요 -> 10으로
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_playerTag))
|
if (string.IsNullOrWhiteSpace(_playerTag)) // 조건이 맞으면 실행할거에요 -> 플레이어 태그가 비어있다면
|
||||||
{
|
{
|
||||||
Debug.LogWarning("[RenderOptimizationConfig] Player Tag가 비어있습니다. 'Player'로 설정됩니다.");
|
Debug.LogWarning("[RenderOptimizationConfig] Player Tag가 비어있습니다. 'Player'로 설정됩니다."); // 경고를 출력할거에요 -> 태그 오류 메시지를
|
||||||
_playerTag = "Player";
|
_playerTag = "Player"; // 값을 설정할거에요 -> "Player"로
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_checkInterval < 0.05f)
|
if (_checkInterval < 0.05f) // 조건이 맞으면 실행할거에요 -> 검사 간격이 0.05보다 작다면
|
||||||
{
|
{
|
||||||
Debug.LogWarning("[RenderOptimizationConfig] Check Interval이 너무 짧습니다. 성능에 영향을 줄 수 있습니다.");
|
Debug.LogWarning("[RenderOptimizationConfig] Check Interval이 너무 짧습니다. 성능에 영향을 줄 수 있습니다."); // 경고를 출력할거에요 -> 성능 영향 메시지를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,14 +86,14 @@ namespace GameSystems.Optimization
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 기본 설정 생성
|
/// 기본 설정 생성
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static RenderOptimizationConfig CreateDefault()
|
public static RenderOptimizationConfig CreateDefault() // 함수를 선언할거에요 -> 기본 설정을 생성하는 CreateDefault를
|
||||||
{
|
{
|
||||||
RenderOptimizationConfig config = CreateInstance<RenderOptimizationConfig>();
|
RenderOptimizationConfig config = CreateInstance<RenderOptimizationConfig>(); // 인스턴스를 생성할거에요 -> RenderOptimizationConfig 객체를
|
||||||
config._renderRange = 40f;
|
config._renderRange = 40f; // 값을 설정할거에요 -> 40으로
|
||||||
config._checkInterval = 0.2f;
|
config._checkInterval = 0.2f; // 값을 설정할거에요 -> 0.2로
|
||||||
config._playerTag = "Player";
|
config._playerTag = "Player"; // 값을 설정할거에요 -> "Player"로
|
||||||
config._showGizmos = true;
|
config._showGizmos = true; // 값을 설정할거에요 -> 참(true)으로
|
||||||
return config;
|
return config; // 반환할거에요 -> 생성된 설정 객체를
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
using System.Collections;
|
using System.Collections; // 컬렉션 기능을 사용할거에요 -> System.Collections를
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic; // 제네릭 컬렉션 기능을 사용할거에요 -> System.Collections.Generic을
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using UnityEngine.SceneManagement;
|
using UnityEngine.SceneManagement; // 씬 관리 기능을 사용할거에요 -> UnityEngine.SceneManagement를
|
||||||
|
|
||||||
public class LoadScene : MonoBehaviour
|
public class LoadScene : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 LoadScene을
|
||||||
{
|
{
|
||||||
[SerializeField] private string sceneName;
|
[SerializeField] private string sceneName; // 변수를 선언할거에요 -> 로드할 씬 이름을 저장할 sceneName을
|
||||||
|
|
||||||
public void LoadOtherScene()
|
public void LoadOtherScene() // 함수를 선언할거에요 -> 다른 씬을 로드하는 LoadOtherScene을
|
||||||
{
|
{
|
||||||
SceneManager.LoadScene(sceneName);
|
SceneManager.LoadScene(sceneName); // 실행할거에요 -> 지정된 이름의 씬을 로드하는 기능을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,22 +1,52 @@
|
||||||
using System.Collections;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using System.Collections.Generic;
|
using UnityEngine.SceneManagement; // 씬 관리 기능을 사용할거에요 -> UnityEngine.SceneManagement를
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
public class GameStopPanelController : MonoBehaviour
|
public class GameStopPanelController : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 GameStopPanelController를
|
||||||
{
|
{
|
||||||
[SerializeField] private GameObject gameStopPanel;
|
[Header("UI 요소")] // 인스펙터 창에 제목을 표시할거에요 -> UI 요소 를
|
||||||
[SerializeField] private SettingPanelController settingPanelController;
|
[SerializeField] private GameObject stopPanel; // 변수를 선언할거에요 -> 일시정지 패널 오브젝트인 stopPanel을
|
||||||
private bool isGameStopping = false;
|
|
||||||
|
|
||||||
private void Update()
|
private bool isPaused = false; // 변수를 초기화할거에요 -> 일시정지 상태 여부를 거짓으로
|
||||||
{
|
|
||||||
if (settingPanelController != null && settingPanelController.IsSettingOpen)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (Input.GetKeyDown(KeyCode.Escape))
|
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를
|
||||||
{
|
{
|
||||||
isGameStopping = !isGameStopping;
|
// ESC 키 입력 감지
|
||||||
gameStopPanel.SetActive(isGameStopping);
|
if (Input.GetKeyDown(KeyCode.Escape)) // 조건이 맞으면 실행할거에요 -> ESC 키를 눌렀다면
|
||||||
|
{
|
||||||
|
TogglePause(); // 함수를 실행할거에요 -> 일시정지 상태를 변경하는 TogglePause를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void TogglePause() // 함수를 선언할거에요 -> 일시정지를 켜고 끄는 TogglePause를
|
||||||
|
{
|
||||||
|
isPaused = !isPaused; // 상태를 뒤집을거에요 -> 일시정지 여부를 반대로
|
||||||
|
|
||||||
|
if (stopPanel != null) stopPanel.SetActive(isPaused); // 설정을 바꿀거에요 -> 패널의 활성화 상태를 isPaused 값대로
|
||||||
|
|
||||||
|
if (isPaused) // 조건이 맞으면 실행할거에요 -> 일시정지 상태라면
|
||||||
|
{
|
||||||
|
Time.timeScale = 0f; // 값을 설정할거에요 -> 시간을 멈춤으로
|
||||||
|
}
|
||||||
|
else // 조건이 틀리면 실행할거에요 -> 게임 진행 상태라면
|
||||||
|
{
|
||||||
|
Time.timeScale = 1f; // 값을 설정할거에요 -> 시간을 정상 속도로
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "메인 메뉴로" 버튼에 연결할 함수
|
||||||
|
public void GoToMainMenu() // 함수를 선언할거에요 -> 메인 메뉴로 이동하는 GoToMainMenu를
|
||||||
|
{
|
||||||
|
Time.timeScale = 1f; // 값을 복구할거에요 -> 멈춘 시간을 정상으로 (씬 이동 전에 필수)
|
||||||
|
SceneManager.LoadScene("MainMenu"); // 실행할거에요 -> "MainMenu" 씬을 로드하는 기능을
|
||||||
|
}
|
||||||
|
|
||||||
|
// "게임 종료" 버튼에 연결할 함수
|
||||||
|
public void QuitGame() // 함수를 선언할거에요 -> 게임을 종료하는 QuitGame을
|
||||||
|
{
|
||||||
|
#if UNITY_EDITOR // 조건부 컴파일을 할거에요 -> 유니티 에디터 환경이라면
|
||||||
|
UnityEditor.EditorApplication.isPlaying = false; // 설정을 바꿀거에요 -> 플레이 모드를 끄기로
|
||||||
|
#else // 그 외 환경(빌드)이라면
|
||||||
|
Application.Quit(); // 실행할거에요 -> 어플리케이션 종료를
|
||||||
|
#endif // 조건부 컴파일을 끝낼거에요
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,19 +1,50 @@
|
||||||
using System.Collections;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using System.Collections.Generic;
|
using UnityEngine.UI; // 유니티 UI 기능을 사용할거에요 -> UnityEngine.UI를
|
||||||
using TMPro;
|
using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
public class GraphicSettingUI : MonoBehaviour
|
public class GraphicSettingUI : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 GraphicSettingUI를
|
||||||
{
|
{
|
||||||
[SerializeField] private TMP_Dropdown qualityDropdown;
|
[Header("전체 화면 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 전체 화면 설정 을
|
||||||
|
[SerializeField] private Toggle fullScreenToggle; // 변수를 선언할거에요 -> 전체 화면 토글 UI를 fullScreenToggle에
|
||||||
|
|
||||||
private void OnEnable()
|
[Header("화질 설정 (드롭다운)")] // 인스펙터 창에 제목을 표시할거에요 -> 화질 설정 (드롭다운) 을
|
||||||
|
[SerializeField] private TMP_Dropdown qualityDropdown; // 변수를 선언할거에요 -> 화질 선택 드롭다운 UI를 qualityDropdown에
|
||||||
|
|
||||||
|
private void Start() // 함수를 실행할거에요 -> 스크립트 시작 시 Start를
|
||||||
{
|
{
|
||||||
qualityDropdown.value = SettingsManager.Instance.qualityIndex;
|
// 현재 설정값 불러와서 UI에 반영
|
||||||
|
LoadCurrentSettings(); // 함수를 실행할거에요 -> 현재 설정을 UI에 표시하는 LoadCurrentSettings를
|
||||||
|
|
||||||
|
// 이벤트 리스너 등록
|
||||||
|
if (fullScreenToggle != null) // 조건이 맞으면 실행할거에요 -> 전체 화면 토글이 있다면
|
||||||
|
fullScreenToggle.onValueChanged.AddListener(OnFullScreenChanged); // 구독할거에요 -> 값 변경 시 OnFullScreenChanged 호출을
|
||||||
|
|
||||||
|
if (qualityDropdown != null) // 조건이 맞으면 실행할거에요 -> 화질 드롭다운이 있다면
|
||||||
|
qualityDropdown.onValueChanged.AddListener(OnQualityChanged); // 구독할거에요 -> 값 변경 시 OnQualityChanged 호출을
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnQualityChanged(int index)
|
private void LoadCurrentSettings() // 함수를 선언할거에요 -> 현재 설정값을 UI에 반영하는 LoadCurrentSettings를
|
||||||
{
|
{
|
||||||
SettingsManager.Instance.SetQuality(index);
|
if (SettingsManager.Instance == null) return; // 조건이 맞으면 중단할거에요 -> 설정 매니저가 없다면
|
||||||
|
|
||||||
|
// 전체 화면 토글 상태 동기화
|
||||||
|
if (fullScreenToggle != null) // 조건이 맞으면 실행할거에요 -> 토글이 있다면
|
||||||
|
fullScreenToggle.isOn = SettingsManager.Instance.isFullScreen; // 값을 설정할거에요 -> 매니저의 전체 화면 설정값으로
|
||||||
|
|
||||||
|
// 화질 드롭다운 값 동기화
|
||||||
|
if (qualityDropdown != null) // 조건이 맞으면 실행할거에요 -> 드롭다운이 있다면
|
||||||
|
qualityDropdown.value = SettingsManager.Instance.qualityIndex; // 값을 설정할거에요 -> 매니저의 화질 인덱스로
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnFullScreenChanged(bool isFull) // 함수를 선언할거에요 -> 전체 화면 변경 시 호출될 OnFullScreenChanged를
|
||||||
|
{
|
||||||
|
if (SettingsManager.Instance != null) // 조건이 맞으면 실행할거에요 -> 매니저가 있다면
|
||||||
|
SettingsManager.Instance.SetFullScreen(isFull); // 실행할거에요 -> 전체 화면 설정을 적용하는 함수를
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnQualityChanged(int index) // 함수를 선언할거에요 -> 화질 변경 시 호출될 OnQualityChanged를
|
||||||
|
{
|
||||||
|
if (SettingsManager.Instance != null) // 조건이 맞으면 실행할거에요 -> 매니저가 있다면
|
||||||
|
SettingsManager.Instance.SetQuality(index); // 실행할거에요 -> 화질 설정을 적용하는 함수를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,37 +1,50 @@
|
||||||
using System.Collections;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using System.Collections.Generic;
|
using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를
|
||||||
using TMPro;
|
using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.UI;
|
|
||||||
|
|
||||||
public class SettingResolutionUI : MonoBehaviour
|
public class SettingResolutionUI : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 SettingResolutionUI를
|
||||||
{
|
{
|
||||||
[SerializeField] private TMP_Dropdown resDropDown;
|
[SerializeField] private TMP_Dropdown resolutionDropdown; // 변수를 선언할거에요 -> 해상도 선택 드롭다운 UI를 resolutionDropdown에
|
||||||
[SerializeField] private Toggle fullScreenToggle;
|
|
||||||
|
|
||||||
private void Start()
|
private Resolution[] resolutions; // 배열을 선언할거에요 -> 사용 가능한 해상도 목록을 resolutions에
|
||||||
|
|
||||||
|
private void Start() // 함수를 실행할거에요 -> 스크립트 시작 시 Start를
|
||||||
{
|
{
|
||||||
var manager = SettingsManager.Instance;
|
InitDropdown(); // 함수를 실행할거에요 -> 드롭다운을 초기화하는 InitDropdown을
|
||||||
|
|
||||||
resDropDown.ClearOptions();
|
|
||||||
|
|
||||||
var options = new List<string>();
|
|
||||||
foreach (var res in manager.FilteredResolutions)
|
|
||||||
options.Add($"{res.width} x {res.height}");
|
|
||||||
|
|
||||||
resDropDown.AddOptions(options);
|
|
||||||
|
|
||||||
resDropDown.value = manager.resolutionIndex;
|
|
||||||
fullScreenToggle.isOn = manager.isFullScreen;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnResolutionChanged(int index)
|
private void InitDropdown() // 함수를 선언할거에요 -> 해상도 목록을 채우는 InitDropdown을
|
||||||
{
|
{
|
||||||
SettingsManager.Instance.SetResolution(index);
|
if (SettingsManager.Instance == null || resolutionDropdown == null) return; // 조건이 맞으면 중단할거에요 -> 매니저나 드롭다운이 없다면
|
||||||
|
|
||||||
|
resolutions = SettingsManager.Instance.FilteredResolutions; // 값을 가져올거에요 -> 매니저에서 필터링된 해상도 목록을
|
||||||
|
resolutionDropdown.ClearOptions(); // 비울거에요 -> 드롭다운의 기존 옵션들을
|
||||||
|
|
||||||
|
List<string> options = new List<string>(); // 리스트를 생성할거에요 -> 옵션 문자열들을 담을 리스트를
|
||||||
|
int currentResolutionIndex = 0; // 변수를 초기화할거에요 -> 현재 해상도 인덱스를 0으로
|
||||||
|
|
||||||
|
for (int i = 0; i < resolutions.Length; i++) // 반복할거에요 -> 모든 해상도에 대해
|
||||||
|
{
|
||||||
|
string option = resolutions[i].width + " x " + resolutions[i].height; // 문자열을 만들거에요 -> "가로 x 세로" 형식으로
|
||||||
|
options.Add(option); // 추가할거에요 -> 만든 문자열을 옵션 리스트에
|
||||||
|
|
||||||
|
// 현재 해상도와 일치하는지 확인
|
||||||
|
if (i == SettingsManager.Instance.resolutionIndex) // 조건이 맞으면 실행할거에요 -> 현재 설정된 인덱스와 같다면
|
||||||
|
{
|
||||||
|
currentResolutionIndex = i; // 값을 저장할거에요 -> 현재 인덱스로
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnFullScreenChanged(bool value)
|
resolutionDropdown.AddOptions(options); // 추가할거에요 -> 완성된 옵션 리스트를 드롭다운에
|
||||||
|
resolutionDropdown.value = currentResolutionIndex; // 값을 설정할거에요 -> 현재 선택된 해상도로
|
||||||
|
resolutionDropdown.RefreshShownValue(); // 갱신할거에요 -> 드롭다운 표시 내용을
|
||||||
|
|
||||||
|
resolutionDropdown.onValueChanged.AddListener(OnResolutionChanged); // 구독할거에요 -> 값 변경 이벤트를
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnResolutionChanged(int index) // 함수를 선언할거에요 -> 해상도 변경 시 호출될 OnResolutionChanged를
|
||||||
{
|
{
|
||||||
SettingsManager.Instance.SetFullScreen(value);
|
if (SettingsManager.Instance != null) // 조건이 맞으면 실행할거에요 -> 매니저가 있다면
|
||||||
|
SettingsManager.Instance.SetResolution(index); // 실행할거에요 -> 해상도 설정을 적용하는 함수를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
using System.Collections;
|
using System.Collections; // 컬렉션 기능을 사용할거에요 -> System.Collections를
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic; // 제네릭 컬렉션 기능을 사용할거에요 -> System.Collections.Generic을
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using UnityEngine.SceneManagement;
|
using UnityEngine.SceneManagement; // 씬 관리 기능을 사용할거에요 -> UnityEngine.SceneManagement를
|
||||||
|
|
||||||
public class SettingsManager : MonoBehaviour
|
public class SettingsManager : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 SettingsManager를
|
||||||
{
|
{
|
||||||
public static SettingsManager Instance;
|
public static SettingsManager Instance; // 변수를 선언할거에요 -> 싱글톤 인스턴스인 Instance를
|
||||||
|
|
||||||
public int qualityIndex;
|
public int qualityIndex; // 변수를 선언할거에요 -> 그래픽 품질 인덱스를 qualityIndex에
|
||||||
public int resolutionIndex;
|
public int resolutionIndex; // 변수를 선언할거에요 -> 해상도 인덱스를 resolutionIndex에
|
||||||
public bool isFullScreen;
|
public bool isFullScreen; // 변수를 선언할거에요 -> 전체 화면 여부를 isFullScreen에
|
||||||
|
|
||||||
private Resolution[] allResolutions;
|
private Resolution[] allResolutions; // 배열을 선언할거에요 -> 지원하는 모든 해상도를 담을 allResolutions를
|
||||||
public Resolution[] FilteredResolutions { get; private set; }
|
public Resolution[] FilteredResolutions { get; private set; } // 프로퍼티를 선언할거에요 -> 필터링된 해상도 목록을 외부에서 읽기만 가능하게
|
||||||
|
|
||||||
private readonly Vector2Int[] commonResolutions =
|
private readonly Vector2Int[] commonResolutions = // 배열을 초기화할거에요 -> 공통 해상도 목록을 읽기 전용으로
|
||||||
{
|
{
|
||||||
new(1280, 720),
|
new(1280, 720),
|
||||||
new(1280, 768),
|
new(1280, 768),
|
||||||
|
|
@ -25,127 +25,127 @@ public class SettingsManager : MonoBehaviour
|
||||||
new(1920, 1080)
|
new(1920, 1080)
|
||||||
};
|
};
|
||||||
|
|
||||||
private void Awake()
|
private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를
|
||||||
{
|
{
|
||||||
Debug.Log("SettingsManager Awake: " + gameObject.name);
|
Debug.Log("SettingsManager Awake: " + gameObject.name); // 로그를 출력할거에요 -> 매니저 시작 알림을
|
||||||
|
|
||||||
if (Instance != null)
|
if (Instance != null) // 조건이 맞으면 실행할거에요 -> 이미 인스턴스가 존재한다면
|
||||||
{
|
{
|
||||||
Destroy(gameObject);
|
Destroy(gameObject); // 파괴할거에요 -> 중복된 이 오브젝트를
|
||||||
return;
|
return; // 중단할거에요 -> 초기화 로직을
|
||||||
}
|
}
|
||||||
|
|
||||||
Instance = this;
|
Instance = this; // 값을 저장할거에요 -> 나 자신을 싱글톤 인스턴스에
|
||||||
DontDestroyOnLoad(gameObject);
|
DontDestroyOnLoad(gameObject); // 유지할거에요 -> 씬이 바뀌어도 파괴되지 않게
|
||||||
|
|
||||||
InitResolutions();
|
InitResolutions(); // 함수를 실행할거에요 -> 해상도 목록을 초기화하는 InitResolutions를
|
||||||
LoadSettings();
|
LoadSettings(); // 함수를 실행할거에요 -> 저장된 설정을 불러오는 LoadSettings를
|
||||||
|
|
||||||
// 🔥 추가
|
// 🔥 추가
|
||||||
SyncResolutionIndexWithCurrentScreen();
|
SyncResolutionIndexWithCurrentScreen(); // 함수를 실행할거에요 -> 현재 화면 해상도와 인덱스를 동기화하는 함수를
|
||||||
|
|
||||||
ApplySettings();
|
ApplySettings(); // 함수를 실행할거에요 -> 설정을 실제로 적용하는 ApplySettings를
|
||||||
}
|
}
|
||||||
|
|
||||||
void InitResolutions()
|
void InitResolutions() // 함수를 선언할거에요 -> 해상도 목록을 초기화하는 InitResolutions를
|
||||||
{
|
{
|
||||||
allResolutions = Screen.resolutions;
|
allResolutions = Screen.resolutions; // 값을 가져올거에요 -> 모니터가 지원하는 모든 해상도를
|
||||||
|
|
||||||
var list = new System.Collections.Generic.List<Resolution>();
|
var list = new System.Collections.Generic.List<Resolution>(); // 리스트를 생성할거에요 -> 해상도를 담을 임시 리스트를
|
||||||
|
|
||||||
foreach (var res in allResolutions)
|
foreach (var res in allResolutions) // 반복할거에요 -> 모든 해상도에 대해
|
||||||
{
|
{
|
||||||
foreach (var common in commonResolutions)
|
foreach (var common in commonResolutions) // 반복할거에요 -> 공통 해상도 목록과 비교하기 위해
|
||||||
{
|
{
|
||||||
if (res.width == common.x && res.height == common.y)
|
if (res.width == common.x && res.height == common.y) // 조건이 맞으면 실행할거에요 -> 가로세로가 공통 해상도와 일치한다면
|
||||||
{
|
{
|
||||||
if (!list.Exists(r => r.width == res.width && r.height == res.height))
|
if (!list.Exists(r => r.width == res.width && r.height == res.height)) // 조건이 맞으면 실행할거에요 -> 리스트에 없는 해상도라면 (중복 방지)
|
||||||
list.Add(res);
|
list.Add(res); // 추가할거에요 -> 리스트에 해당 해상도를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FilteredResolutions = list.ToArray();
|
FilteredResolutions = list.ToArray(); // 변환할거에요 -> 리스트를 배열로 바꿔서 FilteredResolutions에
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 적용 메서드 =====
|
// ===== 적용 메서드 =====
|
||||||
public void SetQuality(int index)
|
public void SetQuality(int index) // 함수를 선언할거에요 -> 그래픽 품질을 설정하는 SetQuality를
|
||||||
{
|
{
|
||||||
qualityIndex = index;
|
qualityIndex = index; // 값을 저장할거에요 -> 품질 인덱스를
|
||||||
QualitySettings.SetQualityLevel(index);
|
QualitySettings.SetQualityLevel(index); // 설정을 바꿀거에요 -> 유니티 품질 설정을 해당 레벨로
|
||||||
PlayerPrefs.SetInt("Quality", index);
|
PlayerPrefs.SetInt("Quality", index); // 저장할거에요 -> 품질 인덱스를 로컬 저장소에
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetResolution(int index)
|
public void SetResolution(int index) // 함수를 선언할거에요 -> 해상도를 설정하는 SetResolution을
|
||||||
{
|
{
|
||||||
resolutionIndex = index;
|
resolutionIndex = index; // 값을 저장할거에요 -> 해상도 인덱스를
|
||||||
ApplyResolution();
|
ApplyResolution(); // 함수를 실행할거에요 -> 해상도를 적용하는 ApplyResolution을
|
||||||
PlayerPrefs.SetInt("Resolution", index);
|
PlayerPrefs.SetInt("Resolution", index); // 저장할거에요 -> 해상도 인덱스를 로컬 저장소에
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetFullScreen(bool full)
|
public void SetFullScreen(bool full) // 함수를 선언할거에요 -> 전체 화면을 설정하는 SetFullScreen을
|
||||||
{
|
{
|
||||||
isFullScreen = full;
|
isFullScreen = full; // 값을 저장할거에요 -> 전체 화면 여부를
|
||||||
|
|
||||||
Screen.fullScreenMode = full
|
Screen.fullScreenMode = full // 설정을 바꿀거에요 -> 화면 모드를
|
||||||
? FullScreenMode.FullScreenWindow
|
? FullScreenMode.FullScreenWindow // 조건이 참이면 전체 화면 창 모드로
|
||||||
: FullScreenMode.Windowed;
|
: FullScreenMode.Windowed; // 조건이 거짓이면 창 모드로
|
||||||
|
|
||||||
ApplyResolution();
|
ApplyResolution(); // 함수를 실행할거에요 -> 해상도 적용 함수를
|
||||||
PlayerPrefs.SetInt("FullScreen", full ? 1 : 0);
|
PlayerPrefs.SetInt("FullScreen", full ? 1 : 0); // 저장할거에요 -> 전체 화면 여부를 (1:참, 0:거짓)
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApplyResolution()
|
void ApplyResolution() // 함수를 선언할거에요 -> 해상도를 실제 적용하는 ApplyResolution을
|
||||||
{
|
{
|
||||||
var res = FilteredResolutions[resolutionIndex];
|
var res = FilteredResolutions[resolutionIndex]; // 값을 가져올거에요 -> 선택된 해상도 정보를
|
||||||
Screen.SetResolution(
|
Screen.SetResolution( // 설정을 바꿀거에요 -> 화면 해상도를
|
||||||
res.width,
|
res.width, // 가로 크기로
|
||||||
res.height,
|
res.height, // 세로 크기로
|
||||||
isFullScreen ? FullScreenMode.FullScreenWindow : FullScreenMode.Windowed
|
isFullScreen ? FullScreenMode.FullScreenWindow : FullScreenMode.Windowed // 화면 모드에 맞춰서
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoadSettings()
|
void LoadSettings() // 함수를 선언할거에요 -> 저장된 설정을 불러오는 LoadSettings를
|
||||||
{
|
{
|
||||||
qualityIndex = PlayerPrefs.GetInt("Quality", QualitySettings.names.Length - 1);
|
qualityIndex = PlayerPrefs.GetInt("Quality", QualitySettings.names.Length - 1); // 불러올거에요 -> 품질 인덱스를 (없으면 최대값)
|
||||||
resolutionIndex = PlayerPrefs.GetInt("Resolution", 0);
|
resolutionIndex = PlayerPrefs.GetInt("Resolution", 0); // 불러올거에요 -> 해상도 인덱스를 (없으면 0)
|
||||||
isFullScreen = PlayerPrefs.GetInt("FullScreen", 1) == 1;
|
isFullScreen = PlayerPrefs.GetInt("FullScreen", 1) == 1; // 불러올거에요 -> 전체 화면 여부를 (없으면 1=참)
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApplySettings()
|
void ApplySettings() // 함수를 선언할거에요 -> 모든 설정을 적용하는 ApplySettings를
|
||||||
{
|
{
|
||||||
SetQuality(qualityIndex);
|
SetQuality(qualityIndex); // 실행할거에요 -> 품질 설정을
|
||||||
Screen.fullScreenMode = isFullScreen
|
Screen.fullScreenMode = isFullScreen // 설정을 바꿀거에요 -> 화면 모드를
|
||||||
? FullScreenMode.FullScreenWindow
|
? FullScreenMode.FullScreenWindow // 조건이 참이면 전체 화면으로
|
||||||
: FullScreenMode.Windowed;
|
: FullScreenMode.Windowed; // 조건이 거짓이면 창 모드로
|
||||||
ApplyResolution();
|
ApplyResolution(); // 실행할거에요 -> 해상도 적용을
|
||||||
}
|
}
|
||||||
|
|
||||||
void SyncResolutionIndexWithCurrentScreen()
|
void SyncResolutionIndexWithCurrentScreen() // 함수를 선언할거에요 -> 현재 화면과 해상도 인덱스를 맞추는 SyncResolutionIndexWithCurrentScreen을
|
||||||
{
|
{
|
||||||
for (int i = 0; i < FilteredResolutions.Length; i++)
|
for (int i = 0; i < FilteredResolutions.Length; i++) // 반복할거에요 -> 필터링된 모든 해상도에 대해
|
||||||
{
|
{
|
||||||
if (FilteredResolutions[i].width == Screen.width &&
|
if (FilteredResolutions[i].width == Screen.width && // 조건이 맞으면 실행할거에요 -> 너비가 현재 화면과 같고
|
||||||
FilteredResolutions[i].height == Screen.height)
|
FilteredResolutions[i].height == Screen.height) // 높이가 현재 화면과 같다면
|
||||||
{
|
{
|
||||||
resolutionIndex = i;
|
resolutionIndex = i; // 값을 저장할거에요 -> 해당 인덱스를
|
||||||
return;
|
return; // 중단할거에요 -> 함수를 (찾았으니까)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnEnable()
|
private void OnEnable() // 함수를 실행할거에요 -> 오브젝트 활성화 시 OnEnable을
|
||||||
{
|
{
|
||||||
SceneManager.sceneLoaded += OnSceneLoaded;
|
SceneManager.sceneLoaded += OnSceneLoaded; // 구독할거에요 -> 씬 로드 이벤트를
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDisable()
|
private void OnDisable() // 함수를 실행할거에요 -> 오브젝트 비활성화 시 OnDisable을
|
||||||
{
|
{
|
||||||
SceneManager.sceneLoaded -= OnSceneLoaded;
|
SceneManager.sceneLoaded -= OnSceneLoaded; // 구독 해제할거에요 -> 씬 로드 이벤트를
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
private void OnSceneLoaded(Scene scene, LoadSceneMode mode) // 함수를 선언할거에요 -> 씬 로드 완료 시 호출될 OnSceneLoaded를
|
||||||
{
|
{
|
||||||
ApplySettings(); // 🔥 씬 로드 직후 다시 적용
|
ApplySettings(); // 🔥 씬 로드 직후 다시 적용 // 실행할거에요 -> 설정 적용 함수를
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,42 +1,42 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using System;
|
using System; // 기본 시스템 기능을 사용할거에요 -> System을
|
||||||
|
|
||||||
public class EnemyHealth : MonoBehaviour, IDamageable
|
public class EnemyHealth : MonoBehaviour, IDamageable // 클래스를 선언할거에요 -> MonoBehaviour와 IDamageable을 상속받는 EnemyHealth를
|
||||||
{
|
{
|
||||||
[Header("--- 능력치 ---")]
|
[Header("--- 능력치 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 능력치 --- 를
|
||||||
[SerializeField] private float maxHealth = 50f;
|
[SerializeField] private float maxHealth = 50f; // 변수를 선언할거에요 -> 최대 체력(50.0)을 maxHealth에
|
||||||
private float _currentHealth;
|
private float _currentHealth; // 변수를 선언할거에요 -> 현재 체력을 저장할 _currentHealth를
|
||||||
|
|
||||||
public event Action<float, float> OnHealthChanged;
|
public event Action<float, float> OnHealthChanged; // 이벤트를 선언할거에요 -> 체력 변경 시 현재/최대를 알릴 OnHealthChanged를
|
||||||
public bool IsDead { get; private set; }
|
public bool IsDead { get; private set; } // 프로퍼티를 선언할거에요 -> 사망 여부를 외부에서 읽기만 가능하게 IsDead에
|
||||||
|
|
||||||
private void Start()
|
private void Start() // 함수를 실행할거에요 -> 시작 시 Start를
|
||||||
{
|
{
|
||||||
_currentHealth = maxHealth;
|
_currentHealth = maxHealth; // 값을 초기화할거에요 -> 현재 체력을 최대 체력으로
|
||||||
// ⭐ 시작하자마자 UI에 현재 숫자가 뜨도록 신호 발송
|
// ⭐ 시작하자마자 UI에 현재 숫자가 뜨도록 신호 발송
|
||||||
OnHealthChanged?.Invoke(_currentHealth, maxHealth);
|
OnHealthChanged?.Invoke(_currentHealth, maxHealth); // 이벤트를 실행할거에요 -> 초기 체력 상태를 알리기 위해
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDamageable 인터페이스 구현
|
// IDamageable 인터페이스 구현
|
||||||
public void TakeDamage(float amount)
|
public void TakeDamage(float amount) // 함수를 선언할거에요 -> 데미지를 입는 TakeDamage를
|
||||||
{
|
{
|
||||||
if (IsDead) return;
|
if (IsDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면
|
||||||
|
|
||||||
_currentHealth = Mathf.Max(0, _currentHealth - amount);
|
_currentHealth = Mathf.Max(0, _currentHealth - amount); // 값을 계산할거에요 -> 체력에서 데미지를 빼고 0 밑으로 안 내려가게
|
||||||
|
|
||||||
// ⭐ 피격 시 로그와 함께 UI 갱신 신호 발송
|
// ⭐ 피격 시 로그와 함께 UI 갱신 신호 발송
|
||||||
Debug.Log($"{gameObject.name} 피격! 체력: {{{_currentHealth}/{maxHealth}}}");
|
Debug.Log($"{gameObject.name} 피격! 체력: {{{_currentHealth}/{maxHealth}}}"); // 로그를 출력할거에요 -> 피격 정보와 남은 체력을
|
||||||
OnHealthChanged?.Invoke(_currentHealth, maxHealth);
|
OnHealthChanged?.Invoke(_currentHealth, maxHealth); // 이벤트를 실행할거에요 -> 변경된 체력 정보를 알리기 위해
|
||||||
|
|
||||||
if (_currentHealth <= 0) Die();
|
if (_currentHealth <= 0) Die(); // 조건이 맞으면 실행할거에요 -> 체력이 0 이하라면 사망 처리 Die를
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Die()
|
private void Die() // 함수를 선언할거에요 -> 사망 처리를 하는 Die를
|
||||||
{
|
{
|
||||||
if (IsDead) return;
|
if (IsDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽음 처리 중이라면
|
||||||
IsDead = true;
|
IsDead = true; // 상태를 바꿀거에요 -> 사망 상태를 참으로
|
||||||
|
|
||||||
Debug.Log($"{gameObject.name} 사망!");
|
Debug.Log($"{gameObject.name} 사망!"); // 로그를 출력할거에요 -> 사망 메시지를
|
||||||
Destroy(gameObject, 1.5f);
|
Destroy(gameObject, 1.5f); // 파괴할거에요 -> 이 오브젝트를 1.5초 뒤에
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,101 +1,100 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
|
|
||||||
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
|
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] // 컴포넌트를 강제로 추가할거에요 -> MeshFilter와 MeshRenderer를
|
||||||
public class ArrowRangeUI : MonoBehaviour
|
public class ArrowRangeUI : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 ArrowRangeUI를
|
||||||
{
|
{
|
||||||
// 🚨 수정됨: 끝부분의 \ 기호를 모두 삭제했습니다.
|
[Header("--- 참조 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 참조 --- 를
|
||||||
[Header("--- 참조 ---")]
|
[SerializeField] private PlayerAttack attackScript; // 변수를 선언할거에요 -> 플레이어 공격 스크립트를 연결할 attackScript를
|
||||||
[SerializeField] private PlayerAttack attackScript; // PlayerAttack 스크립트 연결
|
|
||||||
|
|
||||||
[Header("--- UI 설정 ---")]
|
[Header("--- UI 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- UI 설정 --- 을
|
||||||
[Tooltip("사거리 표시기의 색상")]
|
[Tooltip("사거리 표시기의 색상")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[SerializeField] private Color uiColor = new Color(1f, 1f, 0f, 0.5f); // 노란색 반투명
|
[SerializeField] private Color uiColor = new Color(1f, 1f, 0f, 0.5f); // 변수를 선언할거에요 -> UI 색상(노란색 반투명)을 uiColor에
|
||||||
[Tooltip("화살표 UI의 폭 (두께)")]
|
[Tooltip("화살표 UI의 폭 (두께)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[SerializeField] private float lineWidth = 1.0f;
|
[SerializeField] private float lineWidth = 1.0f; // 변수를 선언할거에요 -> 선 두께(1.0)를 lineWidth에
|
||||||
|
|
||||||
[Header("--- 길이 설정 ---")]
|
[Header("--- 길이 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 길이 설정 --- 을
|
||||||
[Tooltip("차징 0%일 때의 최소 길이 (기본 사거리)")]
|
[Tooltip("차징 0%일 때의 최소 길이 (기본 사거리)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[SerializeField] private float minLength = 5f;
|
[SerializeField] private float minLength = 5f; // 변수를 선언할거에요 -> 최소 길이(5.0)를 minLength에
|
||||||
[Tooltip("풀차징일 때의 최대 길이 (최대 사거리)")]
|
[Tooltip("풀차징일 때의 최대 길이 (최대 사거리)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
||||||
[SerializeField] private float maxLength = 20f;
|
[SerializeField] private float maxLength = 20f; // 변수를 선언할거에요 -> 최대 길이(20.0)를 maxLength에
|
||||||
|
|
||||||
private Mesh _mesh;
|
private Mesh _mesh; // 변수를 선언할거에요 -> UI를 그릴 메쉬를 _mesh에
|
||||||
private MeshFilter _meshFilter;
|
private MeshFilter _meshFilter; // 변수를 선언할거에요 -> 메쉬 필터 컴포넌트를 _meshFilter에
|
||||||
private MeshRenderer _meshRenderer;
|
private MeshRenderer _meshRenderer; // 변수를 선언할거에요 -> 메쉬 렌더러 컴포넌트를 _meshRenderer에
|
||||||
|
|
||||||
private void Awake()
|
private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를
|
||||||
{
|
{
|
||||||
// 메쉬 초기화
|
// 메쉬 초기화
|
||||||
_mesh = new Mesh();
|
_mesh = new Mesh(); // 생성할거에요 -> 빈 메쉬 객체를
|
||||||
_meshFilter = GetComponent<MeshFilter>();
|
_meshFilter = GetComponent<MeshFilter>(); // 컴포넌트를 가져올거에요 -> 메쉬 필터를
|
||||||
_meshRenderer = GetComponent<MeshRenderer>();
|
_meshRenderer = GetComponent<MeshRenderer>(); // 컴포넌트를 가져올거에요 -> 메쉬 렌더러를
|
||||||
_meshFilter.mesh = _mesh;
|
_meshFilter.mesh = _mesh; // 할당할거에요 -> 생성한 메쉬를 필터에
|
||||||
|
|
||||||
// 재질(Material)이 없다면 임시로 생성
|
// 재질(Material)이 없다면 임시로 생성
|
||||||
if (_meshRenderer.material == null)
|
if (_meshRenderer.material == null) // 조건이 맞으면 실행할거에요 -> 렌더러에 재질이 없다면
|
||||||
{
|
{
|
||||||
_meshRenderer.material = new Material(Shader.Find("Sprites/Default"));
|
_meshRenderer.material = new Material(Shader.Find("Sprites/Default")); // 생성할거에요 -> 기본 스프라이트 쉐이더로 새 재질을
|
||||||
}
|
}
|
||||||
_meshRenderer.material.color = uiColor;
|
_meshRenderer.material.color = uiColor; // 색상을 설정할거에요 -> 지정한 UI 색상으로
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를
|
||||||
{
|
{
|
||||||
// 1. 공격 스크립트가 없거나 차징 중이 아니면 끄기
|
// 1. 공격 스크립트가 없거나 차징 중이 아니면 끄기
|
||||||
if (attackScript == null || !attackScript.IsCharging)
|
if (attackScript == null || !attackScript.IsCharging) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 없거나 차징 중이 아니라면
|
||||||
{
|
{
|
||||||
_meshRenderer.enabled = false;
|
_meshRenderer.enabled = false; // 기능을 끌거에요 -> 렌더러를 (안 보이게)
|
||||||
return;
|
return; // 중단할거에요 -> 업데이트를
|
||||||
}
|
}
|
||||||
|
|
||||||
_meshRenderer.enabled = true;
|
_meshRenderer.enabled = true; // 기능을 켤거에요 -> 렌더러를 (보이게)
|
||||||
|
|
||||||
// 2. 차징 진행도(0.0 ~ 1.0) 가져오기
|
// 2. 차징 진행도(0.0 ~ 1.0) 가져오기
|
||||||
float progress = attackScript.ChargeProgress;
|
float progress = attackScript.ChargeProgress; // 값을 가져올거에요 -> 차징 진행률을 progress에
|
||||||
|
|
||||||
// 3. 진행도에 따라 길이 계산 (Lerp로 부드럽게 늘어남)
|
// 3. 진행도에 따라 길이 계산 (Lerp로 부드럽게 늘어남)
|
||||||
float currentLength = Mathf.Lerp(minLength, maxLength, progress);
|
float currentLength = Mathf.Lerp(minLength, maxLength, progress); // 값을 계산할거에요 -> 최소~최대 사이에서 진행도에 맞는 길이를
|
||||||
|
|
||||||
// 4. 직사각형 메쉬 그리기
|
// 4. 직사각형 메쉬 그리기
|
||||||
CreateRectangleMesh(currentLength, lineWidth);
|
CreateRectangleMesh(currentLength, lineWidth); // 함수를 실행할거에요 -> 계산된 길이와 두께로 메쉬를 그리는 CreateRectangleMesh를
|
||||||
}
|
}
|
||||||
|
|
||||||
// 직사각형 메쉬 생성 함수
|
// 직사각형 메쉬 생성 함수
|
||||||
private void CreateRectangleMesh(float length, float width)
|
private void CreateRectangleMesh(float length, float width) // 함수를 선언할거에요 -> 사각형 메쉬를 만드는 CreateRectangleMesh를
|
||||||
{
|
{
|
||||||
Vector3[] vertices = new Vector3[4];
|
Vector3[] vertices = new Vector3[4]; // 배열을 생성할거에요 -> 정점 4개를 담을 vertices를
|
||||||
int[] triangles = new int[6];
|
int[] triangles = new int[6]; // 배열을 생성할거에요 -> 삼각형 인덱스 6개를 담을 triangles를
|
||||||
Vector2[] uvs = new Vector2[4];
|
Vector2[] uvs = new Vector2[4]; // 배열을 생성할거에요 -> UV 좌표 4개를 담을 uvs를
|
||||||
|
|
||||||
float halfWidth = width / 2f;
|
float halfWidth = width / 2f; // 값을 계산할거에요 -> 폭의 절반을 halfWidth에
|
||||||
|
|
||||||
// 정점 4개 정의 (플레이어 발밑 기준 앞으로 뻗어나감)
|
// 정점 4개 정의 (플레이어 발밑 기준 앞으로 뻗어나감)
|
||||||
vertices[0] = new Vector3(-halfWidth, 0.1f, 0);
|
vertices[0] = new Vector3(-halfWidth, 0.1f, 0); // 값을 넣을거에요 -> 왼쪽 아래 정점을 (시작점)
|
||||||
vertices[1] = new Vector3(halfWidth, 0.1f, 0);
|
vertices[1] = new Vector3(halfWidth, 0.1f, 0); // 값을 넣을거에요 -> 오른쪽 아래 정점을 (시작점)
|
||||||
vertices[2] = new Vector3(-halfWidth, 0.1f, length);
|
vertices[2] = new Vector3(-halfWidth, 0.1f, length); // 값을 넣을거에요 -> 왼쪽 위 정점을 (끝점)
|
||||||
vertices[3] = new Vector3(halfWidth, 0.1f, length);
|
vertices[3] = new Vector3(halfWidth, 0.1f, length); // 값을 넣을거에요 -> 오른쪽 위 정점을 (끝점)
|
||||||
|
|
||||||
// 삼각형 연결
|
// 삼각형 연결
|
||||||
triangles[0] = 0;
|
triangles[0] = 0; // 값을 넣을거에요 -> 첫 번째 삼각형의 1번 점 인덱스를
|
||||||
triangles[1] = 2;
|
triangles[1] = 2; // 값을 넣을거에요 -> 첫 번째 삼각형의 2번 점 인덱스를
|
||||||
triangles[2] = 1;
|
triangles[2] = 1; // 값을 넣을거에요 -> 첫 번째 삼각형의 3번 점 인덱스를
|
||||||
|
|
||||||
triangles[3] = 2;
|
triangles[3] = 2; // 값을 넣을거에요 -> 두 번째 삼각형의 1번 점 인덱스를
|
||||||
triangles[4] = 3;
|
triangles[4] = 3; // 값을 넣을거에요 -> 두 번째 삼각형의 2번 점 인덱스를
|
||||||
triangles[5] = 1;
|
triangles[5] = 1; // 값을 넣을거에요 -> 두 번째 삼각형의 3번 점 인덱스를
|
||||||
|
|
||||||
// UV 매핑
|
// UV 매핑
|
||||||
uvs[0] = new Vector2(0, 0);
|
uvs[0] = new Vector2(0, 0); // 값을 넣을거에요 -> 0번 정점의 텍스처 좌표를
|
||||||
uvs[1] = new Vector2(1, 0);
|
uvs[1] = new Vector2(1, 0); // 값을 넣을거에요 -> 1번 정점의 텍스처 좌표를
|
||||||
uvs[2] = new Vector2(0, 1);
|
uvs[2] = new Vector2(0, 1); // 값을 넣을거에요 -> 2번 정점의 텍스처 좌표를
|
||||||
uvs[3] = new Vector2(1, 1);
|
uvs[3] = new Vector2(1, 1); // 값을 넣을거에요 -> 3번 정점의 텍스처 좌표를
|
||||||
|
|
||||||
// 메쉬 적용
|
// 메쉬 적용
|
||||||
_mesh.Clear();
|
_mesh.Clear(); // 비울거에요 -> 기존 메쉬 데이터를
|
||||||
_mesh.vertices = vertices;
|
_mesh.vertices = vertices; // 값을 넣을거에요 -> 새로 만든 정점들을
|
||||||
_mesh.triangles = triangles;
|
_mesh.triangles = triangles; // 값을 넣을거에요 -> 새로 만든 삼각형들을
|
||||||
_mesh.uv = uvs;
|
_mesh.uv = uvs; // 값을 넣을거에요 -> 새로 만든 UV를
|
||||||
_mesh.RecalculateNormals();
|
_mesh.RecalculateNormals(); // 계산할거에요 -> 조명을 위한 법선 벡터를
|
||||||
_mesh.RecalculateBounds();
|
_mesh.RecalculateBounds(); // 계산할거에요 -> 메쉬의 경계 영역을
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,56 +1,93 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 기본 기능(카메라, Mathf 등)을 쓰기 위해 불러올거에요 -> UnityEngine를
|
||||||
using UnityEngine.UI; // ⭐ Image 컴포넌트 사용을 위해 필수!
|
using UnityEngine.UI; // Image 컴포넌트를 쓰기 위해 불러올거에요 -> UnityEngine.UI를
|
||||||
using TMPro;
|
using TMPro; // TextMeshProUGUI를 쓰기 위해 불러올거에요 -> TMPro를
|
||||||
|
|
||||||
public class HPUibar : MonoBehaviour
|
public class HPUibar : MonoBehaviour // 클래스를 선언할거에요 -> 체력바 UI를 갱신하는 HPUibar를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> HPUibar 범위를
|
||||||
[Header("--- 참조 ---")]
|
|
||||||
[SerializeField] private MonoBehaviour healthSource;
|
|
||||||
|
|
||||||
// ⭐ [수정] Slider 대신 Image 타입을 사용합니다.
|
[Header("--- 참조 ---")] // 인스펙터에 제목을 표시할거에요 -> 참조 섹션을
|
||||||
[SerializeField] private Image hpFillImage;
|
[SerializeField] private MonoBehaviour healthSource; // 변수를 선언할거에요 -> 체력 소스(플레이어/몹 등)를 healthSource에
|
||||||
[SerializeField] private TextMeshProUGUI hpText;
|
|
||||||
|
|
||||||
private void Start()
|
[Header("--- UI ---")] // 인스펙터에 제목을 표시할거에요 -> UI 섹션을
|
||||||
{
|
[SerializeField] private Image hpFillImage; // 변수를 선언할거에요 -> 체력 게이지 채움(Image.fillAmount)용 hpFillImage를
|
||||||
// 1. 체력 소스 연결 (플레이어, 더미, 몬스터 등 모두 대응)
|
[SerializeField] private TextMeshProUGUI hpText; // 변수를 선언할거에요 -> 체력 텍스트 표시용 hpText를
|
||||||
if (healthSource is PlayerHealth ph) ph.OnHealthChanged += UpdateUI;
|
|
||||||
else if (healthSource is TrainingDummy td) td.OnHealthChanged += UpdateUI;
|
|
||||||
else if (healthSource is EnemyHealth eh) eh.OnHealthChanged += UpdateUI;
|
|
||||||
else if (healthSource is MonsterClass mc) mc.OnHealthChanged += UpdateUI;
|
|
||||||
|
|
||||||
// 시작 시 풀피로 설정
|
private PlayerHealth _playerHealth; // 변수를 선언할거에요 -> PlayerHealth를 캐싱할 _playerHealth를
|
||||||
if (hpFillImage != null) hpFillImage.fillAmount = 1f;
|
private Stats _playerStats; // 변수를 선언할거에요 -> PlayerHealth의 Stats를 캐싱할 _playerStats를
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateUI(float current, float max)
|
private void Start() // 함수를 선언할거에요 -> 시작 시 1회 실행되는 Start를
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> Start 범위를
|
||||||
// 2. ⭐ [핵심 수정] 이미지의 fillAmount를 0 ~ 1 사이 값으로 조절합니다.
|
|
||||||
if (hpFillImage != null && max > 0)
|
|
||||||
{
|
|
||||||
hpFillImage.fillAmount = current / max;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 텍스트 업데이트
|
// ✅ PlayerHealth는 UnityEvent<float> (ratio만 줌)이므로 AddListener 방식으로 구독할거에요 -> UnityEvent 전용 처리
|
||||||
if (hpText != null)
|
if (healthSource is PlayerHealth ph) // 조건을 검사할거에요 -> healthSource가 PlayerHealth인지
|
||||||
{
|
{ // 코드 블록을 시작할거에요 -> PlayerHealth 처리 범위를
|
||||||
hpText.text = $"{Mathf.CeilToInt(current)} / {Mathf.CeilToInt(max)}";
|
_playerHealth = ph; // 값을 저장할거에요 -> PlayerHealth 캐싱
|
||||||
}
|
_playerStats = ph.GetComponent<Stats>(); // 컴포넌트를 가져올거에요 -> 같은 오브젝트의 Stats를 캐싱
|
||||||
}
|
ph.OnHealthChanged.AddListener(UpdateUIFromRatio); // 이벤트를 구독할거에요 -> UnityEvent<float>라 AddListener 사용
|
||||||
|
ForceRefreshPlayerUI(); // 시작 시 UI를 강제로 한 번 맞출거에요 -> CurrentHP 기반으로
|
||||||
|
} // 코드 블록을 끝낼거에요 -> PlayerHealth 처리
|
||||||
|
|
||||||
private void LateUpdate()
|
// ✅ 나머지는 (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로
|
||||||
if (Camera.main != null)
|
else if (healthSource is MonsterClass mc) mc.OnHealthChanged += UpdateUI; // 몬스터면 (current,max) 이벤트 구독할거에요 -> UpdateUI로
|
||||||
transform.LookAt(transform.position + Camera.main.transform.forward);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDestroy()
|
if (hpFillImage != null) hpFillImage.fillAmount = 1f; // 조건이 맞으면 실행할거에요 -> 시작 시 게이지를 풀로 표시
|
||||||
{
|
} // 코드 블록을 끝낼거에요 -> Start를
|
||||||
// 이벤트 해제 (메모리 누수 방지)
|
|
||||||
if (healthSource is PlayerHealth ph) ph.OnHealthChanged -= UpdateUI;
|
private void ForceRefreshPlayerUI() // 함수를 선언할거에요 -> PlayerHealth용 UI 강제 갱신 함수 ForceRefreshPlayerUI를
|
||||||
else if (healthSource is TrainingDummy td) td.OnHealthChanged -= UpdateUI;
|
{ // 코드 블록을 시작할거에요 -> ForceRefreshPlayerUI 범위를
|
||||||
else if (healthSource is EnemyHealth eh) eh.OnHealthChanged -= UpdateUI;
|
if (_playerHealth == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어가 없으면
|
||||||
else if (healthSource is MonsterClass mc) mc.OnHealthChanged -= UpdateUI;
|
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를
|
||||||
|
|
@ -1,43 +1,43 @@
|
||||||
using UnityEngine;
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
||||||
using TMPro;
|
using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를
|
||||||
|
|
||||||
public class PlayerStatsUI : MonoBehaviour
|
public class PlayerStatsUI : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerStatsUI를
|
||||||
{
|
{
|
||||||
[Header("--- 데이터 연결 ---")]
|
[Header("--- 데이터 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 데이터 연결 --- 을
|
||||||
[SerializeField] private Stats playerStats;
|
[SerializeField] private Stats playerStats; // 변수를 선언할거에요 -> 플레이어 스탯 스크립트를
|
||||||
|
|
||||||
[Header("--- UI 오브젝트 ---")]
|
[Header("--- UI 오브젝트 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- UI 오브젝트 --- 를
|
||||||
[SerializeField] private GameObject statWindowPanel;
|
[SerializeField] private GameObject statWindowPanel; // 변수를 선언할거에요 -> 스탯 창 패널 오브젝트를
|
||||||
|
|
||||||
[Header("--- 텍스트 UI ---")]
|
[Header("--- 텍스트 UI ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 텍스트 UI --- 를
|
||||||
[SerializeField] private TextMeshProUGUI maxHealthText;
|
[SerializeField] private TextMeshProUGUI maxHealthText; // 변수를 선언할거에요 -> 최대 체력 텍스트 UI를
|
||||||
// [제거] strengthText 변수는 인스펙터에서 비워두거나 삭제하세요.
|
// [제거] strengthText 변수는 인스펙터에서 비워두거나 삭제하세요.
|
||||||
[SerializeField] private TextMeshProUGUI damageText;
|
[SerializeField] private TextMeshProUGUI damageText; // 변수를 선언할거에요 -> 데미지 텍스트 UI를
|
||||||
[SerializeField] private TextMeshProUGUI speedText;
|
[SerializeField] private TextMeshProUGUI speedText; // 변수를 선언할거에요 -> 이동속도 텍스트 UI를
|
||||||
|
|
||||||
private void Start()
|
private void Start() // 함수를 실행할거에요 -> 스크립트 시작 시 Start를
|
||||||
{
|
{
|
||||||
if (statWindowPanel != null) statWindowPanel.SetActive(false);
|
if (statWindowPanel != null) statWindowPanel.SetActive(false); // 조건이 맞으면 실행할거에요 -> 스탯 창이 있다면 처음에 끄기를
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ToggleWindow()
|
public void ToggleWindow() // 함수를 선언할거에요 -> 스탯 창을 켜고 끄는 ToggleWindow를
|
||||||
{
|
{
|
||||||
if (statWindowPanel == null) return;
|
if (statWindowPanel == null) return; // 조건이 맞으면 중단할거에요 -> 패널이 연결되지 않았다면
|
||||||
bool isActive = !statWindowPanel.activeSelf;
|
bool isActive = !statWindowPanel.activeSelf; // 값을 반전시킬거에요 -> 현재 활성화 상태를 뒤집어서
|
||||||
statWindowPanel.SetActive(isActive);
|
statWindowPanel.SetActive(isActive); // 설정을 바꿀거에요 -> 패널의 활성화 상태를
|
||||||
if (isActive) UpdateStatTexts();
|
if (isActive) UpdateStatTexts(); // 조건이 맞으면 실행할거에요 -> 창이 켜졌다면 텍스트 갱신을
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateStatTexts()
|
public void UpdateStatTexts() // 함수를 선언할거에요 -> 스탯 텍스트들을 갱신하는 UpdateStatTexts를
|
||||||
{
|
{
|
||||||
if (playerStats == null) return;
|
if (playerStats == null) return; // 조건이 맞으면 중단할거에요 -> 스탯 데이터가 없다면
|
||||||
|
|
||||||
maxHealthText.text = $"MaxHP: {playerStats.MaxHealth}";
|
maxHealthText.text = $"MaxHP: {playerStats.MaxHealth}"; // 텍스트를 바꿀거에요 -> 최대 체력 수치로
|
||||||
// ✨ [수정] 힘 텍스트 업데이트 로직 삭제
|
// ✨ [수정] 힘 텍스트 업데이트 로직 삭제
|
||||||
|
|
||||||
damageText.text = $"Damage: {playerStats.TotalAttackDamage} (+{playerStats.weaponDamage})";
|
damageText.text = $"Damage: {playerStats.TotalAttackDamage} (+{playerStats.weaponDamage})"; // 텍스트를 바꿀거에요 -> 총 공격력과 무기 공격력으로
|
||||||
|
|
||||||
// ✨ [수정] 무게 페널티 없이 현재 속도만 깔끔하게 표기
|
// ✨ [수정] 무게 페널티 없이 현재 속도만 깔끔하게 표기
|
||||||
speedText.text = $"Speed: {playerStats.CurrentMoveSpeed:F1}";
|
speedText.text = $"Speed: {playerStats.CurrentMoveSpeed:F1}"; // 텍스트를 바꿀거에요 -> 현재 이동 속도로 (소수점 1자리)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user