角色能力,存档
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
description: "编写或修改 C# 代码时,确保命名、注释、Tooltip、Header 中不出现其他游戏的专有名称或明确引用。适用于所有 .cs 文件。"
|
||||
applyTo: "**/*.cs"
|
||||
name: no-game-references
|
||||
description: '编写或修改 C# 代码时加载此规则:确保命名、注释、Tooltip、Header 中不出现其他游戏的专有名称(如 HK / Hollow Knight / Silksong / Ori 等)或明确引用。触发场景:新建 .cs 文件、修改已有 .cs 文件、Code Review .cs 代码是否合规。'
|
||||
---
|
||||
|
||||
# 代码命名与注释:禁止引用其他游戏
|
||||
26
Assets/_Game/Data/Camera/Main Camera Custom Blends.asset
Normal file
26
Assets/_Game/Data/Camera/Main Camera Custom Blends.asset
Normal file
@@ -0,0 +1,26 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 36baaa8bdcb9d8b49b9199833965d2c3, type: 3}
|
||||
m_Name: Main Camera Custom Blends
|
||||
m_EditorClassIdentifier:
|
||||
CustomBlends:
|
||||
- From: '**ANY CAMERA**'
|
||||
To: '**ANY CAMERA**'
|
||||
Blend:
|
||||
Style: 1
|
||||
Time: 1
|
||||
CustomCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve: []
|
||||
m_PreInfinity: 0
|
||||
m_PostInfinity: 0
|
||||
m_RotationOrder: 0
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c8d85f4a62eaed04abaa7f5bea0ebccc
|
||||
guid: 9906ca91115b34d498a040782ef36e92
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
@@ -10,9 +10,9 @@ MonoBehaviour:
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 96b10c11e6173394a8fa8d9c614b0035, type: 3}
|
||||
m_Name: DS_TestEnemyBody
|
||||
m_Name: CMB_Player_Attack1
|
||||
m_EditorClassIdentifier:
|
||||
sourceId: enemy_body
|
||||
sourceId:
|
||||
skillId:
|
||||
BaseDamage: 10
|
||||
DamageMultiplier: 1
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a0e13ba45445ebe408ef0353f5adcbfb
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,28 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 96b10c11e6173394a8fa8d9c614b0035, type: 3}
|
||||
m_Name: CMB_Player_Attack2
|
||||
m_EditorClassIdentifier:
|
||||
sourceId:
|
||||
skillId:
|
||||
BaseDamage: 10
|
||||
DamageMultiplier: 1
|
||||
Type: 0
|
||||
Category: 0
|
||||
Flags: 2
|
||||
Tags: 1
|
||||
KnockbackForce: 5
|
||||
HitStunDuration: 0.1
|
||||
BreakLevel: 1
|
||||
FxType: 1
|
||||
ComboWindowDuration: 0.4
|
||||
CancelWindowEnd: 0.5
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d4b575099a307fb46b19cf67f8b1e76c
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,28 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 96b10c11e6173394a8fa8d9c614b0035, type: 3}
|
||||
m_Name: CMB_Player_Attack3
|
||||
m_EditorClassIdentifier:
|
||||
sourceId:
|
||||
skillId:
|
||||
BaseDamage: 10
|
||||
DamageMultiplier: 1
|
||||
Type: 0
|
||||
Category: 0
|
||||
Flags: 2
|
||||
Tags: 1
|
||||
KnockbackForce: 5
|
||||
HitStunDuration: 0.1
|
||||
BreakLevel: 1
|
||||
FxType: 1
|
||||
ComboWindowDuration: 0.4
|
||||
CancelWindowEnd: 0.5
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f5dc1ab0247ba343885d3b83c80da8a
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
139
Assets/_Game/Data/Combat/Weapons/WPN_DiHun.asset
Normal file
139
Assets/_Game/Data/Combat/Weapons/WPN_DiHun.asset
Normal file
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d2443d04d1c179d4d8a4f36e7ca7156e, type: 3}
|
||||
m_Name: WPN_DiHun
|
||||
m_EditorClassIdentifier:
|
||||
weaponId: WPN_DiHun
|
||||
displayName:
|
||||
icon: {fileID: 0}
|
||||
weaponType: 1
|
||||
groundComboSteps:
|
||||
- clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 7400000, guid: bde6a54ec8fc04f45a424f9bcd7845df, type: 2}
|
||||
_NormalizedStartTime: NaN
|
||||
damageSource: {fileID: 11400000, guid: a0e13ba45445ebe408ef0353f5adcbfb, type: 2}
|
||||
hitBoxEnter: 0
|
||||
hitBoxExit: 0
|
||||
comboInputOpen: 0
|
||||
comboInputClose: 0
|
||||
cancelWindowOpen: 0
|
||||
recoveryTime: 0
|
||||
comboTimeout: 0
|
||||
hitBoxId:
|
||||
- clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 7400000, guid: c0ef319f521c2a2448022cebcf9c9266, type: 2}
|
||||
_NormalizedStartTime: NaN
|
||||
damageSource: {fileID: 11400000, guid: d4b575099a307fb46b19cf67f8b1e76c, type: 2}
|
||||
hitBoxEnter: 0.032
|
||||
hitBoxExit: 0.669
|
||||
comboInputOpen: 0.057
|
||||
comboInputClose: 0.292
|
||||
cancelWindowOpen: 0.061
|
||||
recoveryTime: 0
|
||||
comboTimeout: 0
|
||||
hitBoxId:
|
||||
airComboSteps:
|
||||
- clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
damageSource: {fileID: 0}
|
||||
hitBoxEnter: 0.1
|
||||
hitBoxExit: 0.8
|
||||
comboInputOpen: 0
|
||||
comboInputClose: 0
|
||||
cancelWindowOpen: 0
|
||||
recoveryTime: 0.05
|
||||
comboTimeout: 0
|
||||
hitBoxId:
|
||||
- clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
damageSource: {fileID: 0}
|
||||
hitBoxEnter: 0.1
|
||||
hitBoxExit: 0.8
|
||||
comboInputOpen: 0
|
||||
comboInputClose: 0
|
||||
cancelWindowOpen: 0
|
||||
recoveryTime: 0.05
|
||||
comboTimeout: 0
|
||||
hitBoxId:
|
||||
upStep:
|
||||
clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
damageSource: {fileID: 0}
|
||||
hitBoxEnter: 0.2
|
||||
hitBoxExit: 0.7
|
||||
comboInputOpen: 0
|
||||
comboInputClose: 0
|
||||
cancelWindowOpen: 0
|
||||
recoveryTime: 0.05
|
||||
comboTimeout: 0
|
||||
hitBoxId:
|
||||
downStep:
|
||||
clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
damageSource: {fileID: 0}
|
||||
hitBoxEnter: 0.1
|
||||
hitBoxExit: 0.9
|
||||
comboInputOpen: 0
|
||||
comboInputClose: 0
|
||||
cancelWindowOpen: 0
|
||||
recoveryTime: 0.05
|
||||
comboTimeout: 0
|
||||
hitBoxId:
|
||||
hitBoxPrefab: {fileID: 4821376343125962025, guid: 2fce42e66b8d4a042a471a661a05ee1b, type: 3}
|
||||
vfxConfig:
|
||||
onEquipPresetId:
|
||||
weaponTrailPrefab: {fileID: 0}
|
||||
trailColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
soulPowerGain: 10
|
||||
references:
|
||||
version: 2
|
||||
RefIds: []
|
||||
8
Assets/_Game/Data/Combat/Weapons/WPN_DiHun.asset.meta
Normal file
8
Assets/_Game/Data/Combat/Weapons/WPN_DiHun.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bde7d85bdf2d3e54da22d07b1f8d2901
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
103
Assets/_Game/Data/Combat/Weapons/WPN_MingHun.asset
Normal file
103
Assets/_Game/Data/Combat/Weapons/WPN_MingHun.asset
Normal file
@@ -0,0 +1,103 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d2443d04d1c179d4d8a4f36e7ca7156e, type: 3}
|
||||
m_Name: WPN_MingHun
|
||||
m_EditorClassIdentifier:
|
||||
weaponId: WPN_MingHun
|
||||
displayName:
|
||||
icon: {fileID: 0}
|
||||
weaponType: 2
|
||||
groundComboSteps:
|
||||
- clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
damageSource: {fileID: 0}
|
||||
hitBoxEnter: 0.3
|
||||
hitBoxExit: 0.6
|
||||
comboInputOpen: 0.3
|
||||
comboInputClose: 0
|
||||
cancelWindowOpen: 0.5
|
||||
recoveryTime: 0.05
|
||||
comboTimeout: 0.25
|
||||
hitBoxId:
|
||||
airComboSteps:
|
||||
- clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
damageSource: {fileID: 0}
|
||||
hitBoxEnter: 0.1
|
||||
hitBoxExit: 0.8
|
||||
comboInputOpen: 0
|
||||
comboInputClose: 0
|
||||
cancelWindowOpen: 0
|
||||
recoveryTime: 0.05
|
||||
comboTimeout: 0
|
||||
hitBoxId:
|
||||
upStep:
|
||||
clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
damageSource: {fileID: 0}
|
||||
hitBoxEnter: 0.2
|
||||
hitBoxExit: 0.7
|
||||
comboInputOpen: 0
|
||||
comboInputClose: 0
|
||||
cancelWindowOpen: 0
|
||||
recoveryTime: 0.05
|
||||
comboTimeout: 0
|
||||
hitBoxId:
|
||||
downStep:
|
||||
clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
damageSource: {fileID: 0}
|
||||
hitBoxEnter: 0.1
|
||||
hitBoxExit: 0.9
|
||||
comboInputOpen: 0
|
||||
comboInputClose: 0
|
||||
cancelWindowOpen: 0
|
||||
recoveryTime: 0.05
|
||||
comboTimeout: 0
|
||||
hitBoxId:
|
||||
hitBoxPrefab: {fileID: 0}
|
||||
vfxConfig:
|
||||
onEquipPresetId:
|
||||
weaponTrailPrefab: {fileID: 0}
|
||||
trailColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
soulPowerGain: 10
|
||||
references:
|
||||
version: 2
|
||||
RefIds: []
|
||||
8
Assets/_Game/Data/Combat/Weapons/WPN_MingHun.asset.meta
Normal file
8
Assets/_Game/Data/Combat/Weapons/WPN_MingHun.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fbe1ff6f23c995541a5833b51c52dc01
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
103
Assets/_Game/Data/Combat/Weapons/WPN_TianHun.asset
Normal file
103
Assets/_Game/Data/Combat/Weapons/WPN_TianHun.asset
Normal file
@@ -0,0 +1,103 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d2443d04d1c179d4d8a4f36e7ca7156e, type: 3}
|
||||
m_Name: WPN_TianHun
|
||||
m_EditorClassIdentifier:
|
||||
weaponId: WPN_TianHun
|
||||
displayName:
|
||||
icon: {fileID: 0}
|
||||
weaponType: 0
|
||||
groundComboSteps:
|
||||
- clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
damageSource: {fileID: 0}
|
||||
hitBoxEnter: 0.3
|
||||
hitBoxExit: 0.6
|
||||
comboInputOpen: 0.3
|
||||
comboInputClose: 0
|
||||
cancelWindowOpen: 0.5
|
||||
recoveryTime: 0.05
|
||||
comboTimeout: 0.25
|
||||
hitBoxId:
|
||||
airComboSteps:
|
||||
- clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
damageSource: {fileID: 0}
|
||||
hitBoxEnter: 0.1
|
||||
hitBoxExit: 0.8
|
||||
comboInputOpen: 0
|
||||
comboInputClose: 0
|
||||
cancelWindowOpen: 0
|
||||
recoveryTime: 0.05
|
||||
comboTimeout: 0
|
||||
hitBoxId:
|
||||
upStep:
|
||||
clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
damageSource: {fileID: 0}
|
||||
hitBoxEnter: 0.2
|
||||
hitBoxExit: 0.7
|
||||
comboInputOpen: 0
|
||||
comboInputClose: 0
|
||||
cancelWindowOpen: 0
|
||||
recoveryTime: 0.05
|
||||
comboTimeout: 0
|
||||
hitBoxId:
|
||||
downStep:
|
||||
clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
damageSource: {fileID: 0}
|
||||
hitBoxEnter: 0.1
|
||||
hitBoxExit: 0.9
|
||||
comboInputOpen: 0
|
||||
comboInputClose: 0
|
||||
cancelWindowOpen: 0
|
||||
recoveryTime: 0.05
|
||||
comboTimeout: 0
|
||||
hitBoxId:
|
||||
hitBoxPrefab: {fileID: 0}
|
||||
vfxConfig:
|
||||
onEquipPresetId:
|
||||
weaponTrailPrefab: {fileID: 0}
|
||||
trailColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
soulPowerGain: 10
|
||||
references:
|
||||
version: 2
|
||||
RefIds: []
|
||||
8
Assets/_Game/Data/Combat/Weapons/WPN_TianHun.asset.meta
Normal file
8
Assets/_Game/Data/Combat/Weapons/WPN_TianHun.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 533d4711e21d8584597a5d4569fe2eb0
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
16
Assets/_Game/Data/Player/PLY_EquipmentConfig.asset
Normal file
16
Assets/_Game/Data/Player/PLY_EquipmentConfig.asset
Normal file
@@ -0,0 +1,16 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d2ff92bffe90e0f499b70bdb9d045552, type: 3}
|
||||
m_Name: PLY_EquipmentConfig
|
||||
m_EditorClassIdentifier:
|
||||
initialNotchCount: 3
|
||||
maxCollectionSize: -1
|
||||
8
Assets/_Game/Data/Player/PLY_EquipmentConfig.asset.meta
Normal file
8
Assets/_Game/Data/Player/PLY_EquipmentConfig.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f167dd4c0f40ff7499127f917066994a
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -13,6 +13,6 @@ MonoBehaviour:
|
||||
m_Name: PLY_FormConfig
|
||||
m_EditorClassIdentifier:
|
||||
forms:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- {fileID: 11400000, guid: b2ba655d7ab18bc48b2855c56d25cc1b, type: 2}
|
||||
- {fileID: 11400000, guid: 82d209d1343ca524dbf22fe56c489246, type: 2}
|
||||
- {fileID: 11400000, guid: 76b37db14bfccc2488ad29268d02d54a, type: 2}
|
||||
|
||||
@@ -12,23 +12,20 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 5ec15df6b0d345c4f92ba459e89dc02f, type: 3}
|
||||
m_Name: PLY_PlayerAnimationConfig
|
||||
m_EditorClassIdentifier:
|
||||
Idle: {fileID: 0}
|
||||
Run: {fileID: 0}
|
||||
Jump: {fileID: 0}
|
||||
Fall: {fileID: 0}
|
||||
Dash: {fileID: 0}
|
||||
WallSlide: {fileID: 0}
|
||||
Hurt: {fileID: 0}
|
||||
Dead: {fileID: 0}
|
||||
Idle: {fileID: 7400000, guid: 0fba2927fa4627449b8e277805c0b3b5, type: 2}
|
||||
Run: {fileID: 7400000, guid: 8f7b8659b100c93499761109be066543, type: 2}
|
||||
Jump: {fileID: 7400000, guid: 8ef02c7477ee3ea4585a4d0bcc20ab42, type: 2}
|
||||
AirJump: {fileID: 7400000, guid: 904dfb09ecf693d45aea778ecfbd8eb7, type: 2}
|
||||
Fall: {fileID: 7400000, guid: 525660d3a8f083c49b48fce0f82f604e, type: 2}
|
||||
Dash: {fileID: 7400000, guid: 407a732f86675164a9d61ac1b61fb8bf, type: 2}
|
||||
DashInvincible: {fileID: 7400000, guid: 612dcd6dfa8c62743bbd1e9fc30071ae, type: 2}
|
||||
WallSlide: {fileID: 7400000, guid: fb9661f2571db684b8a09c25f884a31f, type: 2}
|
||||
WallJumpAway: {fileID: 7400000, guid: 8ef02c7477ee3ea4585a4d0bcc20ab42, type: 2}
|
||||
WallJumpToward: {fileID: 7400000, guid: 8ef02c7477ee3ea4585a4d0bcc20ab42, type: 2}
|
||||
Hurt: {fileID: 7400000, guid: 3f4bb0b6b3ca2224f9697775b61c8ab6, type: 2}
|
||||
Dead: {fileID: 7400000, guid: 720c61e4274ae5745b3777cbc7f2fcea, type: 2}
|
||||
HurtDuration: 0.4
|
||||
UseSpring: {fileID: 0}
|
||||
GroundAttacks: []
|
||||
GroundAttackTimings:
|
||||
- HitBoxEnter: 0.3
|
||||
HitBoxExit: 0.6
|
||||
AirAttack: {fileID: 0}
|
||||
UpAttack: {fileID: 0}
|
||||
DownAttack: {fileID: 0}
|
||||
ParryStart: {fileID: 0}
|
||||
ParrySuccess: {fileID: 0}
|
||||
SwimIdle: {fileID: 0}
|
||||
|
||||
@@ -12,21 +12,27 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 81da55e0fcf99d34693cbc5a348225c3, type: 3}
|
||||
m_Name: PLY_PlayerMovementConfig
|
||||
m_EditorClassIdentifier:
|
||||
RunSpeed: 7
|
||||
Acceleration: 50
|
||||
Deceleration: 80
|
||||
JumpForce: 18
|
||||
RunSpeed: 10
|
||||
AirDragFactor: 1
|
||||
JumpForce: 24
|
||||
CoyoteTime: 0.12
|
||||
FallGravityMult: 2.5
|
||||
MaxFallSpeed: 20
|
||||
MaxFallSpeed: 28
|
||||
JumpCutMultiplier: 0.321
|
||||
ApexThreshold: 3
|
||||
ApexGravityMultiplier: 0.3
|
||||
MaxAirJumps: 1
|
||||
DoubleJumpForce: 19
|
||||
DashSpeed: 20
|
||||
DashDuration: 0.18
|
||||
DashDuration: 0.25
|
||||
DashCooldown: 0.4
|
||||
MaxAerialDashes: 1
|
||||
DashInvincibilityDuration: 0.2
|
||||
DashInvincibilityCooldown: 0.9
|
||||
WallSlideSpeed: 2
|
||||
WallJumpForceX: 12
|
||||
WallJumpForceY: 16
|
||||
WallRayLength: 0.55
|
||||
WallRayLength: 0.37
|
||||
WallRayOffsetY: 0.2
|
||||
WallGrabMaxHeightGain: 0.5
|
||||
WallGrabReleaseDelay: 0.08
|
||||
@@ -34,4 +40,4 @@ MonoBehaviour:
|
||||
WallJumpAwayForceX: 10
|
||||
WallJumpAwayForceY: 18
|
||||
WallJumpInputLockDuration: 0.15
|
||||
DefaultGravityScale: 3
|
||||
DefaultGravityScale: 6
|
||||
|
||||
24
Assets/_Game/Data/Player/PLY_PlayerStats.asset
Normal file
24
Assets/_Game/Data/Player/PLY_PlayerStats.asset
Normal file
@@ -0,0 +1,24 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 31a9f22bef1315643bf5a49f2a6edd2b, type: 3}
|
||||
m_Name: PLY_PlayerStats
|
||||
m_EditorClassIdentifier:
|
||||
MaxHP: 5
|
||||
MaxSoulPower: 100
|
||||
MaxSpiritPower: 100
|
||||
SpiritRegenRate: 5
|
||||
MaxSpringCharges: 3
|
||||
SpringHealAmount: 2
|
||||
SpringKillThreshold: 4
|
||||
InvincibilityDuration: 0.6
|
||||
InitialLingZhu: 0
|
||||
InitialAbilities: 524279
|
||||
8
Assets/_Game/Data/Player/PLY_PlayerStats.asset.meta
Normal file
8
Assets/_Game/Data/Player/PLY_PlayerStats.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eaaee0817c0cc9e449142241ad75827e
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/_Game/Data/Player/Weapons.meta
Normal file
8
Assets/_Game/Data/Player/Weapons.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf52457264252d34db476527ee76c8d2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
103
Assets/_Game/Data/Player/Weapons/Weapon_DiHun.asset
Normal file
103
Assets/_Game/Data/Player/Weapons/Weapon_DiHun.asset
Normal file
@@ -0,0 +1,103 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d2443d04d1c179d4d8a4f36e7ca7156e, type: 3}
|
||||
m_Name: Weapon_DiHun
|
||||
m_EditorClassIdentifier:
|
||||
weaponId:
|
||||
displayName:
|
||||
icon: {fileID: 0}
|
||||
weaponType: 0
|
||||
groundComboSteps:
|
||||
- clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
damageSource: {fileID: 0}
|
||||
hitBoxEnter: 0.3
|
||||
hitBoxExit: 0.6
|
||||
comboInputOpen: 0.3
|
||||
comboInputClose: 0
|
||||
cancelWindowOpen: 0.5
|
||||
recoveryTime: 0.05
|
||||
comboTimeout: 0.25
|
||||
hitBoxId:
|
||||
airComboSteps:
|
||||
- clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
damageSource: {fileID: 0}
|
||||
hitBoxEnter: 0.1
|
||||
hitBoxExit: 0.8
|
||||
comboInputOpen: 0
|
||||
comboInputClose: 0
|
||||
cancelWindowOpen: 0
|
||||
recoveryTime: 0.05
|
||||
comboTimeout: 0
|
||||
hitBoxId:
|
||||
upStep:
|
||||
clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
damageSource: {fileID: 0}
|
||||
hitBoxEnter: 0.2
|
||||
hitBoxExit: 0.7
|
||||
comboInputOpen: 0
|
||||
comboInputClose: 0
|
||||
cancelWindowOpen: 0
|
||||
recoveryTime: 0.05
|
||||
comboTimeout: 0
|
||||
hitBoxId:
|
||||
downStep:
|
||||
clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
damageSource: {fileID: 0}
|
||||
hitBoxEnter: 0.1
|
||||
hitBoxExit: 0.9
|
||||
comboInputOpen: 0
|
||||
comboInputClose: 0
|
||||
cancelWindowOpen: 0
|
||||
recoveryTime: 0.05
|
||||
comboTimeout: 0
|
||||
hitBoxId:
|
||||
hitBoxPrefab: {fileID: 0}
|
||||
vfxConfig:
|
||||
onEquipPresetId:
|
||||
weaponTrailPrefab: {fileID: 0}
|
||||
trailColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
soulPowerGain: 10
|
||||
references:
|
||||
version: 2
|
||||
RefIds: []
|
||||
8
Assets/_Game/Data/Player/Weapons/Weapon_DiHun.asset.meta
Normal file
8
Assets/_Game/Data/Player/Weapons/Weapon_DiHun.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f80486c9cd3d1db459ce6df915b11546
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
87
Assets/_Game/Data/Player/Weapons/Weapon_MingHun.asset
Normal file
87
Assets/_Game/Data/Player/Weapons/Weapon_MingHun.asset
Normal file
@@ -0,0 +1,87 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d2443d04d1c179d4d8a4f36e7ca7156e, type: 3}
|
||||
m_Name: Weapon_MingHun
|
||||
m_EditorClassIdentifier:
|
||||
weaponId:
|
||||
displayName:
|
||||
icon: {fileID: 0}
|
||||
weaponType: 0
|
||||
attack1Clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
attack2Clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
attack3Clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
airAttackClip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
upAttackClip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
downAttackClip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
attack1Source: {fileID: 0}
|
||||
attack2Source: {fileID: 0}
|
||||
attack3Source: {fileID: 0}
|
||||
airAttackSource: {fileID: 0}
|
||||
upAttackSource: {fileID: 0}
|
||||
downAttackSource: {fileID: 0}
|
||||
hitBoxPrefab: {fileID: 0}
|
||||
vfxConfig:
|
||||
onEquipPresetId:
|
||||
weaponTrailPrefab: {fileID: 0}
|
||||
trailColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
soulPowerGain: 10
|
||||
references:
|
||||
version: 2
|
||||
RefIds: []
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d87ae01ed8a2b4f4cb27d159a52d1a14
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
87
Assets/_Game/Data/Player/Weapons/Weapon_TianHun.asset
Normal file
87
Assets/_Game/Data/Player/Weapons/Weapon_TianHun.asset
Normal file
@@ -0,0 +1,87 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d2443d04d1c179d4d8a4f36e7ca7156e, type: 3}
|
||||
m_Name: Weapon_TianHun
|
||||
m_EditorClassIdentifier:
|
||||
weaponId:
|
||||
displayName:
|
||||
icon: {fileID: 0}
|
||||
weaponType: 0
|
||||
attack1Clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
attack2Clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
attack3Clip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
airAttackClip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
upAttackClip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
downAttackClip:
|
||||
_FadeDuration: 0.25
|
||||
_Speed: 1
|
||||
_Events:
|
||||
_NormalizedTimes: []
|
||||
_Callbacks: []
|
||||
_Names: []
|
||||
_Clip: {fileID: 0}
|
||||
_NormalizedStartTime: NaN
|
||||
attack1Source: {fileID: 0}
|
||||
attack2Source: {fileID: 0}
|
||||
attack3Source: {fileID: 0}
|
||||
airAttackSource: {fileID: 0}
|
||||
upAttackSource: {fileID: 0}
|
||||
downAttackSource: {fileID: 0}
|
||||
hitBoxPrefab: {fileID: 0}
|
||||
vfxConfig:
|
||||
onEquipPresetId:
|
||||
weaponTrailPrefab: {fileID: 0}
|
||||
trailColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
soulPowerGain: 10
|
||||
references:
|
||||
version: 2
|
||||
RefIds: []
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 76de8c0ba36dcce4db8990c5b62e9ed8
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
15
Assets/_Game/Data/Progression/Charms/CHM_Catalog.asset
Normal file
15
Assets/_Game/Data/Progression/Charms/CHM_Catalog.asset
Normal file
@@ -0,0 +1,15 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 277db74c72224f149b8805dc31b2f944, type: 3}
|
||||
m_Name: CHM_Catalog
|
||||
m_EditorClassIdentifier:
|
||||
_charms: []
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62b057558c311d649ba7d5d91633b544
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
8
Assets/_Game/Prefabs/Weapons.meta
Normal file
8
Assets/_Game/Prefabs/Weapons.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 53a03bf20d81ee84394d0bde462f5b98
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
632
Assets/_Game/Prefabs/Weapons/WPN_WPN_DiHun_HitBox.prefab
Normal file
632
Assets/_Game/Prefabs/Weapons/WPN_WPN_DiHun_HitBox.prefab
Normal file
@@ -0,0 +1,632 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &467203328547477162
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7119158475861943178}
|
||||
- component: {fileID: 7882116945389632025}
|
||||
- component: {fileID: 4639356286286040131}
|
||||
m_Layer: 8
|
||||
m_Name: HitBox_Ground_1
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &7119158475861943178
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 467203328547477162}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0.798, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 8975424752584779179}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!61 &7882116945389632025
|
||||
BoxCollider2D:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 467203328547477162}
|
||||
m_Enabled: 1
|
||||
m_Density: 1
|
||||
m_Material: {fileID: 0}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_LayerOverridePriority: 0
|
||||
m_ForceSendLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ForceReceiveLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ContactCaptureLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_CallbackLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_IsTrigger: 1
|
||||
m_UsedByEffector: 0
|
||||
m_UsedByComposite: 0
|
||||
m_Offset: {x: 0, y: -0.16736698}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0, y: 0}
|
||||
oldSize: {x: 0, y: 0}
|
||||
newSize: {x: 0, y: 0}
|
||||
adaptiveTilingThreshold: 0
|
||||
drawMode: 0
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
serializedVersion: 2
|
||||
m_Size: {x: 1, y: 0.83473396}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!114 &4639356286286040131
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 467203328547477162}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_defaultSource: {fileID: 0}
|
||||
_hitCooldown: 0.1
|
||||
_id:
|
||||
_rivalHitBoxMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 32832
|
||||
--- !u!1 &1932889250901504761
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 6088225995420515986}
|
||||
- component: {fileID: 4288322330375545731}
|
||||
- component: {fileID: 6478051166999031478}
|
||||
m_Layer: 8
|
||||
m_Name: HitBox_Down
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &6088225995420515986
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1932889250901504761}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: -0.562, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 8975424752584779179}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!61 &4288322330375545731
|
||||
BoxCollider2D:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1932889250901504761}
|
||||
m_Enabled: 1
|
||||
m_Density: 1
|
||||
m_Material: {fileID: 0}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_LayerOverridePriority: 0
|
||||
m_ForceSendLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ForceReceiveLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ContactCaptureLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_CallbackLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_IsTrigger: 1
|
||||
m_UsedByEffector: 0
|
||||
m_UsedByComposite: 0
|
||||
m_Offset: {x: -0.027121663, y: -0.15051937}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0, y: 0}
|
||||
oldSize: {x: 0, y: 0}
|
||||
newSize: {x: 0, y: 0}
|
||||
adaptiveTilingThreshold: 0
|
||||
drawMode: 0
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
serializedVersion: 2
|
||||
m_Size: {x: 1.189852, y: 0.80103874}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!114 &6478051166999031478
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1932889250901504761}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_defaultSource: {fileID: 0}
|
||||
_hitCooldown: 0.1
|
||||
_id:
|
||||
_rivalHitBoxMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
--- !u!1 &2584603199706918030
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1660186156348129284}
|
||||
- component: {fileID: 1152578598430080845}
|
||||
- component: {fileID: 3007294148525084107}
|
||||
m_Layer: 8
|
||||
m_Name: HitBox_Ground _2
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &1660186156348129284
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2584603199706918030}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0.798, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 8975424752584779179}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!61 &1152578598430080845
|
||||
BoxCollider2D:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2584603199706918030}
|
||||
m_Enabled: 1
|
||||
m_Density: 1
|
||||
m_Material: {fileID: 0}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_LayerOverridePriority: 0
|
||||
m_ForceSendLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ForceReceiveLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ContactCaptureLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_CallbackLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_IsTrigger: 1
|
||||
m_UsedByEffector: 0
|
||||
m_UsedByComposite: 0
|
||||
m_Offset: {x: -0.117884755, y: 0.01309824}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0, y: 0}
|
||||
oldSize: {x: 0, y: 0}
|
||||
newSize: {x: 0, y: 0}
|
||||
adaptiveTilingThreshold: 0
|
||||
drawMode: 0
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
serializedVersion: 2
|
||||
m_Size: {x: 0.7642305, y: 1.1956644}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!114 &3007294148525084107
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2584603199706918030}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_defaultSource: {fileID: 0}
|
||||
_hitCooldown: 0.1
|
||||
_id:
|
||||
_rivalHitBoxMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 32832
|
||||
--- !u!1 &4050057806632877121
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 4405470499151834857}
|
||||
- component: {fileID: 8597809172682257212}
|
||||
- component: {fileID: 1610035618021234136}
|
||||
m_Layer: 8
|
||||
m_Name: HitBox_Air_2
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &4405470499151834857
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4050057806632877121}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0.553, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 8975424752584779179}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!61 &8597809172682257212
|
||||
BoxCollider2D:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4050057806632877121}
|
||||
m_Enabled: 1
|
||||
m_Density: 1
|
||||
m_Material: {fileID: 0}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_LayerOverridePriority: 0
|
||||
m_ForceSendLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ForceReceiveLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ContactCaptureLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_CallbackLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_IsTrigger: 1
|
||||
m_UsedByEffector: 0
|
||||
m_UsedByComposite: 0
|
||||
m_Offset: {x: 0.27943045, y: 0}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0, y: 0}
|
||||
oldSize: {x: 0, y: 0}
|
||||
newSize: {x: 0, y: 0}
|
||||
adaptiveTilingThreshold: 0
|
||||
drawMode: 0
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
serializedVersion: 2
|
||||
m_Size: {x: 1.0588609, y: 1}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!114 &1610035618021234136
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4050057806632877121}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_defaultSource: {fileID: 0}
|
||||
_hitCooldown: 0.1
|
||||
_id:
|
||||
_rivalHitBoxMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
--- !u!1 &4335406389674002762
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7468586589501741901}
|
||||
- component: {fileID: 4473084966422755714}
|
||||
- component: {fileID: 1392799324577637263}
|
||||
m_Layer: 8
|
||||
m_Name: HitBox_Up
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &7468586589501741901
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4335406389674002762}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0.918, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 8975424752584779179}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!61 &4473084966422755714
|
||||
BoxCollider2D:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4335406389674002762}
|
||||
m_Enabled: 1
|
||||
m_Density: 1
|
||||
m_Material: {fileID: 0}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_LayerOverridePriority: 0
|
||||
m_ForceSendLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ForceReceiveLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ContactCaptureLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_CallbackLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_IsTrigger: 1
|
||||
m_UsedByEffector: 0
|
||||
m_UsedByComposite: 0
|
||||
m_Offset: {x: 0.072324514, y: 0}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0, y: 0}
|
||||
oldSize: {x: 0, y: 0}
|
||||
newSize: {x: 0, y: 0}
|
||||
adaptiveTilingThreshold: 0
|
||||
drawMode: 0
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
serializedVersion: 2
|
||||
m_Size: {x: 1.1599612, y: 1}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!114 &1392799324577637263
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4335406389674002762}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_defaultSource: {fileID: 0}
|
||||
_hitCooldown: 0.1
|
||||
_id:
|
||||
_rivalHitBoxMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
--- !u!1 &4821376343125962025
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 8975424752584779179}
|
||||
- component: {fileID: 3691925044832415471}
|
||||
m_Layer: 0
|
||||
m_Name: WPN_WPN_DiHun_HitBox
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &8975424752584779179
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4821376343125962025}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 7119158475861943178}
|
||||
- {fileID: 1660186156348129284}
|
||||
- {fileID: 7468586589501741901}
|
||||
- {fileID: 6088225995420515986}
|
||||
- {fileID: 4362395311111627733}
|
||||
- {fileID: 4405470499151834857}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &3691925044832415471
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4821376343125962025}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: ec12dacf2519f58429dd3c59da8f93b0, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_hitBoxGround: {fileID: 4639356286286040131}
|
||||
_hitBoxUp: {fileID: 1392799324577637263}
|
||||
_hitBoxDown: {fileID: 6478051166999031478}
|
||||
_hitBoxAir: {fileID: 9014207169512774676}
|
||||
--- !u!1 &8582289489283119946
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 4362395311111627733}
|
||||
- component: {fileID: 922051492914393482}
|
||||
- component: {fileID: 9014207169512774676}
|
||||
m_Layer: 8
|
||||
m_Name: HitBox_Air_1
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &4362395311111627733
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8582289489283119946}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0.553, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 8975424752584779179}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!61 &922051492914393482
|
||||
BoxCollider2D:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8582289489283119946}
|
||||
m_Enabled: 1
|
||||
m_Density: 1
|
||||
m_Material: {fileID: 0}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_LayerOverridePriority: 0
|
||||
m_ForceSendLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ForceReceiveLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ContactCaptureLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_CallbackLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_IsTrigger: 1
|
||||
m_UsedByEffector: 0
|
||||
m_UsedByComposite: 0
|
||||
m_Offset: {x: 0.46717286, y: 0}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0, y: 0}
|
||||
oldSize: {x: 0, y: 0}
|
||||
newSize: {x: 0, y: 0}
|
||||
adaptiveTilingThreshold: 0
|
||||
drawMode: 0
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
serializedVersion: 2
|
||||
m_Size: {x: 1.4343457, y: 1}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!114 &9014207169512774676
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8582289489283119946}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_defaultSource: {fileID: 0}
|
||||
_hitCooldown: 0.1
|
||||
_id:
|
||||
_rivalHitBoxMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2fce42e66b8d4a042a471a661a05ee1b
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -351,11 +351,7 @@ MonoBehaviour:
|
||||
_brain: {fileID: 533647439}
|
||||
_impulseSource: {fileID: 51085361}
|
||||
_lookSystem: {fileID: 843380225}
|
||||
_vcamA: {fileID: 2015076228}
|
||||
_vcamB: {fileID: 852869520}
|
||||
_globalActivePriority: 10
|
||||
_standbyPriority: 1
|
||||
_defaultBlendProfile: {fileID: 0}
|
||||
_defaultBlendProfile: {fileID: 11400000, guid: 33f7ac6591bc7db4ea52d89d3441b567, type: 2}
|
||||
_lensConfig: {fileID: 11400000, guid: 12fec951ce5cc3d499b00e38b5dfa14a, type: 2}
|
||||
_onPlayerSpawned: {fileID: 11400000, guid: 7e2c7e614f6627b449a244ab44443adf, type: 2}
|
||||
_showDebugOverlay: 1
|
||||
@@ -1014,6 +1010,7 @@ GameObject:
|
||||
- component: {fileID: 533647440}
|
||||
- component: {fileID: 533647439}
|
||||
- component: {fileID: 533647443}
|
||||
- component: {fileID: 533647444}
|
||||
m_Layer: 0
|
||||
m_Name: Main Camera
|
||||
m_TagString: MainCamera
|
||||
@@ -1052,7 +1049,7 @@ MonoBehaviour:
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
CustomBlends: {fileID: 0}
|
||||
CustomBlends: {fileID: 11400000, guid: 9906ca91115b34d498a040782ef36e92, type: 2}
|
||||
--- !u!81 &533647440
|
||||
AudioListener:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -1096,7 +1093,7 @@ Camera:
|
||||
far clip plane: 5000
|
||||
field of view: 10
|
||||
orthographic: 0
|
||||
orthographic size: 10
|
||||
orthographic size: 8.4375
|
||||
m_Depth: 0
|
||||
m_CullingMask:
|
||||
serializedVersion: 2
|
||||
@@ -1121,7 +1118,7 @@ Transform:
|
||||
m_GameObject: {fileID: 533647438}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: -64}
|
||||
m_LocalPosition: {x: 122.45427, y: -35, z: -64}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
@@ -1171,6 +1168,30 @@ MonoBehaviour:
|
||||
m_MipBias: 0
|
||||
m_VarianceClampScale: 0.9
|
||||
m_ContrastAdaptiveSharpening: 0
|
||||
--- !u!114 &533647444
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 533647438}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: c88f5cead0c0b2a4eb05b5900433f8d1, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_ComponentVersion: 1
|
||||
m_AssetsPPU: 32
|
||||
m_RefResolutionX: 320
|
||||
m_RefResolutionY: 180
|
||||
m_CropFrame: 4
|
||||
m_GridSnapping: 0
|
||||
m_FilterMode: 0
|
||||
m_UpscaleRT: 0
|
||||
m_PixelSnapping: 0
|
||||
m_CropFrameX: 0
|
||||
m_CropFrameY: 0
|
||||
m_StretchFill: 0
|
||||
--- !u!1 &555895481
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -1535,182 +1556,6 @@ MonoBehaviour:
|
||||
_lookSpeedH: 2
|
||||
_resetSpeedH: 5
|
||||
_speedGateThreshold: 2.5
|
||||
--- !u!1 &852869513
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 852869514}
|
||||
- component: {fileID: 852869520}
|
||||
- component: {fileID: 852869519}
|
||||
- component: {fileID: 852869518}
|
||||
- component: {fileID: 852869517}
|
||||
- component: {fileID: 852869516}
|
||||
- component: {fileID: 852869515}
|
||||
m_Layer: 0
|
||||
m_Name: VCamB
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &852869514
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 852869513}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: -64}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 990528702}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &852869515
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 852869513}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a12cbb2380ff137459b7ba80d492733f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_restScale: 0.25
|
||||
_speedAtFullLookahead: 12
|
||||
_speedSmoothing: 5
|
||||
--- !u!114 &852869516
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 852869513}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: cb5a7225ab133e74b81d1f0ae22ccc77, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_dampingDown: 0.08
|
||||
_dampingUp: 0.65
|
||||
--- !u!114 &852869517
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 852869513}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 7e2e7849ca8d76f438c4b2899c9fb421, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
LockX: 0
|
||||
LockY: 0
|
||||
LockedX: 0
|
||||
LockedY: 0
|
||||
--- !u!114 &852869518
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 852869513}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f453f694addf4275988fac205bc91968, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
BoundingShape2D: {fileID: 0}
|
||||
Damping: 0.5
|
||||
SlowingDistance: 5
|
||||
OversizeWindow:
|
||||
Enabled: 1
|
||||
MaxWindowSize: 0.001
|
||||
Padding: 0
|
||||
m_LegacyMaxWindowSize: -2
|
||||
--- !u!114 &852869519
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 852869513}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 886251e9a18ece04ea8e61686c173e1b, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
CameraDistance: 64
|
||||
DeadZoneDepth: 0
|
||||
Composition:
|
||||
ScreenPosition: {x: 0, y: -0.15}
|
||||
DeadZone:
|
||||
Enabled: 1
|
||||
Size: {x: 0.15, y: 0.05}
|
||||
HardLimits:
|
||||
Enabled: 0
|
||||
Size: {x: 0.8, y: 0.8}
|
||||
Offset: {x: 0, y: 0}
|
||||
CenterOnActivate: 1
|
||||
TargetOffset: {x: 0, y: 0, z: 0}
|
||||
Damping: {x: 0.5, y: 0, z: 0}
|
||||
Lookahead:
|
||||
Enabled: 1
|
||||
Time: 0.28
|
||||
Smoothing: 5
|
||||
IgnoreY: 1
|
||||
--- !u!114 &852869520
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 852869513}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f9dfa5b682dcd46bda6128250e975f58, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
Priority:
|
||||
Enabled: 0
|
||||
m_Value: 0
|
||||
OutputChannel: 1
|
||||
StandbyUpdate: 2
|
||||
m_StreamingVersion: 20241001
|
||||
m_LegacyPriority: 0
|
||||
Target:
|
||||
TrackingTarget: {fileID: 0}
|
||||
LookAtTarget: {fileID: 0}
|
||||
CustomLookAtTarget: 0
|
||||
Lens:
|
||||
FieldOfView: 10
|
||||
OrthographicSize: 10
|
||||
NearClipPlane: 0.1
|
||||
FarClipPlane: 5000
|
||||
Dutch: 0
|
||||
ModeOverride: 0
|
||||
PhysicalProperties:
|
||||
GateFit: 2
|
||||
SensorSize: {x: 21.946, y: 16.002}
|
||||
LensShift: {x: 0, y: 0}
|
||||
FocusDistance: 10
|
||||
Iso: 200
|
||||
ShutterSpeed: 0.005
|
||||
Aperture: 16
|
||||
BladeCount: 5
|
||||
Curvature: {x: 2, y: 11}
|
||||
BarrelClipping: 0.25
|
||||
Anamorphism: 0
|
||||
BlendHint: 0
|
||||
--- !u!1 &990528701
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -1743,8 +1588,6 @@ Transform:
|
||||
- {fileID: 533647442}
|
||||
- {fileID: 51085360}
|
||||
- {fileID: 843380224}
|
||||
- {fileID: 2015076222}
|
||||
- {fileID: 852869514}
|
||||
m_Father: {fileID: 313736080}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1021425710
|
||||
@@ -2808,182 +2651,6 @@ Canvas:
|
||||
m_SortingLayerID: 0
|
||||
m_SortingOrder: 0
|
||||
m_TargetDisplay: 0
|
||||
--- !u!1 &2015076221
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 2015076222}
|
||||
- component: {fileID: 2015076228}
|
||||
- component: {fileID: 2015076227}
|
||||
- component: {fileID: 2015076226}
|
||||
- component: {fileID: 2015076225}
|
||||
- component: {fileID: 2015076224}
|
||||
- component: {fileID: 2015076223}
|
||||
m_Layer: 0
|
||||
m_Name: VCamA
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &2015076222
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2015076221}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: -64}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 990528702}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &2015076223
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2015076221}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a12cbb2380ff137459b7ba80d492733f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_restScale: 0.25
|
||||
_speedAtFullLookahead: 12
|
||||
_speedSmoothing: 5
|
||||
--- !u!114 &2015076224
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2015076221}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: cb5a7225ab133e74b81d1f0ae22ccc77, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_dampingDown: 0.08
|
||||
_dampingUp: 0.65
|
||||
--- !u!114 &2015076225
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2015076221}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 7e2e7849ca8d76f438c4b2899c9fb421, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
LockX: 0
|
||||
LockY: 0
|
||||
LockedX: 0
|
||||
LockedY: 0
|
||||
--- !u!114 &2015076226
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2015076221}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f453f694addf4275988fac205bc91968, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
BoundingShape2D: {fileID: 0}
|
||||
Damping: 0.5
|
||||
SlowingDistance: 5
|
||||
OversizeWindow:
|
||||
Enabled: 1
|
||||
MaxWindowSize: 0.001
|
||||
Padding: 0
|
||||
m_LegacyMaxWindowSize: -2
|
||||
--- !u!114 &2015076227
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2015076221}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 886251e9a18ece04ea8e61686c173e1b, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
CameraDistance: 64
|
||||
DeadZoneDepth: 0
|
||||
Composition:
|
||||
ScreenPosition: {x: 0, y: -0.15}
|
||||
DeadZone:
|
||||
Enabled: 1
|
||||
Size: {x: 0.15, y: 0.05}
|
||||
HardLimits:
|
||||
Enabled: 0
|
||||
Size: {x: 0.8, y: 0.8}
|
||||
Offset: {x: 0, y: 0}
|
||||
CenterOnActivate: 1
|
||||
TargetOffset: {x: 0, y: 0, z: 0}
|
||||
Damping: {x: 0.5, y: 0, z: 0}
|
||||
Lookahead:
|
||||
Enabled: 1
|
||||
Time: 0.28
|
||||
Smoothing: 5
|
||||
IgnoreY: 1
|
||||
--- !u!114 &2015076228
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2015076221}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f9dfa5b682dcd46bda6128250e975f58, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
Priority:
|
||||
Enabled: 0
|
||||
m_Value: 0
|
||||
OutputChannel: 1
|
||||
StandbyUpdate: 2
|
||||
m_StreamingVersion: 20241001
|
||||
m_LegacyPriority: 0
|
||||
Target:
|
||||
TrackingTarget: {fileID: 0}
|
||||
LookAtTarget: {fileID: 0}
|
||||
CustomLookAtTarget: 0
|
||||
Lens:
|
||||
FieldOfView: 10
|
||||
OrthographicSize: 10
|
||||
NearClipPlane: 0.1
|
||||
FarClipPlane: 5000
|
||||
Dutch: 0
|
||||
ModeOverride: 0
|
||||
PhysicalProperties:
|
||||
GateFit: 2
|
||||
SensorSize: {x: 21.946, y: 16.002}
|
||||
LensShift: {x: 0, y: 0}
|
||||
FocusDistance: 10
|
||||
Iso: 200
|
||||
ShutterSpeed: 0.005
|
||||
Aperture: 16
|
||||
BladeCount: 5
|
||||
Curvature: {x: 2, y: 11}
|
||||
BarrelClipping: 0.25
|
||||
Anamorphism: 0
|
||||
BlendHint: 0
|
||||
--- !u!1 &2038852547
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -77,7 +77,7 @@ namespace BaseGames.Camera
|
||||
|
||||
float rawSpeedX = Mathf.Abs(follow.position.x - _lastFollowX) / deltaTime;
|
||||
_lastFollowX = follow.position.x;
|
||||
_estimatedSpeedX = Mathf.Lerp(_estimatedSpeedX, rawSpeedX, deltaTime * _speedSmoothing);
|
||||
_estimatedSpeedX = Mathf.Lerp(_estimatedSpeedX, rawSpeedX, 1f - Mathf.Exp(-deltaTime * _speedSmoothing));
|
||||
}
|
||||
|
||||
// ── 速度映射 → Lookahead 时长 ─────────────────────────────────────
|
||||
|
||||
@@ -5,20 +5,19 @@ namespace BaseGames.Camera
|
||||
{
|
||||
/// <summary>
|
||||
/// 相机区域数据组件。一个房间场景内可放置任意数量的 CameraArea,
|
||||
/// 每个区域独立定义限位范围、可视边界与混合配置。
|
||||
/// 每个区域独立定义限位范围、可视边界、跟随参数与混合配置。
|
||||
///
|
||||
/// 运行时由 <see cref="CameraStateController"/> 管理:
|
||||
/// - <c>_dedicatedCamera</c> 为空 → 使用 Persistent 场景中的两台全局 VCam 交替承接,
|
||||
/// 减少场景内 VCam 数量,相机参数统一由全局 VCam 配置。
|
||||
/// - <c>_dedicatedCamera</c> 不为空 → 激活该专有 VCam(优先级高于全局 VCam),
|
||||
/// 适用于需要独特相机参数(FOV / Offset / 阻尼)的特殊区域。
|
||||
/// 每个区域均拥有专属的 <c>_dedicatedCamera</c>,进入该区域时激活其专属 VCam。
|
||||
/// <c>OverrideFollowBehaviour</c> 为真时,<see cref="CameraStateController"/> 会将本组件中的
|
||||
/// 跟随参数应用到专属 VCam;否则 VCam 保持 Inspector 中的默认值。
|
||||
/// </summary>
|
||||
public class CameraArea : MonoBehaviour
|
||||
{
|
||||
[Header("限位区域")]
|
||||
[Tooltip("定义相机移动边界的 PolygonCollider2D(通常挂载在子节点 AreaBoundary 上)。\n" +
|
||||
"会被赋给全局 VCam 的 CinemachineConfiner2D.BoundingShape2D。")]
|
||||
[SerializeField] private PolygonCollider2D _confinerCollider;
|
||||
[Tooltip("定义相机移动边界的 BoxCollider(通常挂载在子节点 AreaBoundary 上)。\n" +
|
||||
"会被绑定到专属 VCam 的 CinemachineConfiner3D.BoundingVolume。")]
|
||||
[SerializeField] private BoxCollider _confinerCollider;
|
||||
|
||||
[Header("可视区域(透视相机)")]
|
||||
[Tooltip("摄像机应显示的最大可视矩形(本地坐标,相对于此 GameObject 的 Transform 位置)。\n" +
|
||||
@@ -30,8 +29,7 @@ namespace BaseGames.Camera
|
||||
[SerializeField] private float _cameraDepth = 0f;
|
||||
|
||||
[Header("镜头配置")]
|
||||
[Tooltip("全局相机镜头参数 SO。与 CameraStateController 引用同一资产,\n" +
|
||||
"保证 FOV 等参数在 Room 场景中也能正确读取。\n" +
|
||||
[Tooltip("相机镜头参数 SO,提供 FOV、相机深度等参数。\n" +
|
||||
"SO 中的 fieldOfView 发生变化时,编辑器会自动重新同步限位多边形。")]
|
||||
[SerializeField] private CameraLensConfigSO _lensConfig;
|
||||
|
||||
@@ -40,9 +38,9 @@ namespace BaseGames.Camera
|
||||
[SerializeField] private float _lastSyncFOV = 0f;
|
||||
// ── 跟随行为 ──────────────────────────────────────────────────────────
|
||||
|
||||
[Header("跟随行为(覆盖全局 VCam 参数)")]
|
||||
[Tooltip("启用后,进入此区域时将把以下参数写入全局 VCam;\n" +
|
||||
"关闭则 VCam 保持上一区域或 Inspector 中的默认值。")]
|
||||
[Header("跟随行为(专属 VCam 参数)")]
|
||||
[Tooltip("启用后,激活此区域时将以下参数应用到专属 VCam;\n" +
|
||||
"关闭则 VCam 保持 Inspector 中的默认参数不作任何覆写。")]
|
||||
[SerializeField] private bool _overrideFollowBehaviour = true;
|
||||
|
||||
[Tooltip("玩家跟踪点在屏幕上的位置(0 = 中心,±0.5 = 边缘)。\n" +
|
||||
@@ -78,6 +76,20 @@ namespace BaseGames.Camera
|
||||
+ "防止相机因偏置超出 Confiner 边界。")]
|
||||
[SerializeField] private bool _disableFallBias = false;
|
||||
|
||||
// ── 方向感知水平偏置 ──────────────────────────────────────────────────
|
||||
|
||||
[Header("方向感知偏置(需配合 CameraFacingBiasExtension)")]
|
||||
[Tooltip("是否覆盖此区域的方向感知水平偏置量。\n"
|
||||
+ "关闭时使用 CameraFacingBiasExtension 组件的默认值;\n"
|
||||
+ "开启后可将此区域的偏置设为 0(禁用)或更小值(如窄走廊)。")]
|
||||
[SerializeField] private bool _overrideFacingBias = false;
|
||||
|
||||
[Tooltip("方向感知水平偏置量(世界单位)。\n"
|
||||
+ "0 = 禁用此区域的方向偏置(推荐用于宽度受限的走廊)。\n"
|
||||
+ "仅在 Override Facing Bias = true 时生效。")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float _facingBiasOverride = 0f;
|
||||
|
||||
// ── 轴向约束 ──────────────────────────────────────────────────────────
|
||||
|
||||
[Header("轴向约束")]
|
||||
@@ -87,8 +99,9 @@ namespace BaseGames.Camera
|
||||
[Tooltip("锁定相机 Y 轴(水平走廊:相机仅左右移动,Y 固定在限位区域中心)。")]
|
||||
[SerializeField] private bool _lockVertical = false;
|
||||
|
||||
[Header("镜头尺寸(正交相机)")]
|
||||
[Tooltip("进入此区域时的目标正交尺寸(0 = 不覆盖当前尺寸)。\n" +
|
||||
[Header("镜头尺寸(视野缩放)")]
|
||||
[Tooltip("进入此区域时的目标可视半高(世界单位,0 = 不覆盖)。\n" +
|
||||
"等价于正交相机的 OrthographicSize,透视相机下自动换算为 FOV。\n" +
|
||||
"适用于 Boss 战拉远或精密解谜区域拉近。")]
|
||||
[SerializeField] private float _lensSize = 0f;
|
||||
|
||||
@@ -99,18 +112,31 @@ namespace BaseGames.Camera
|
||||
[Header("混合配置")]
|
||||
[SerializeField] private CameraBlendProfileSO _blendProfile;
|
||||
|
||||
[Header("专有虚拟相机(可选)")]
|
||||
[Tooltip("为空时由全局双 VCam 交替过渡(推荐,节省 VCam 数量)。\n" +
|
||||
"不为空时激活此专有 CinemachineCamera,优先级高于全局 VCam。\n" +
|
||||
"适用于需要独特 FOV / Noise / LookAt 等参数的特殊区域。")]
|
||||
[Header("相机噪音(氛围震动)")]
|
||||
[Tooltip("此区域的相机噪音配置(Noise Settings 资产)。\n"
|
||||
+ "洞穴、水下、机械区等需要氛围震动时使用;留空则禁用噪音(AmplitudeGain = 0)。\n"
|
||||
+ "专属 VCam 已包含 CinemachineBasicMultiChannelPerlin 组件(使用工具创建时自动附加)。")]
|
||||
[SerializeField] private NoiseSettings _noiseProfile;
|
||||
|
||||
[Tooltip("噪音振幅增益(0 = 无震动,推荐 0.2 ~ 0.8)。")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float _noiseAmplitude = 0.5f;
|
||||
|
||||
[Tooltip("噪音频率增益(倍率;1 = 资产原始频率,越大震动越快)。")]
|
||||
[Min(0.01f)]
|
||||
[SerializeField] private float _noiseFrequency = 1f;
|
||||
|
||||
[Header("虚拟相机")]
|
||||
[Tooltip("此区域的专属 CinemachineCamera。\n" +
|
||||
"每个区域均应配置自己的专属 VCam,可通过编辑器工具(Camera Area Setup)一键创建。")]
|
||||
[SerializeField] private CinemachineCamera _dedicatedCamera;
|
||||
|
||||
[Tooltip("专有 VCam 激活时使用的优先级,须高于全局 VCam 的 _globalActivePriority(默认 10)。")]
|
||||
[Tooltip("专属 VCam 激活时使用的优先级,默认 20。")]
|
||||
[SerializeField] private int _dedicatedPriority = 20;
|
||||
|
||||
// ── 公开属性 ──────────────────────────────────────────────────────────
|
||||
|
||||
public PolygonCollider2D ConfinerCollider => _confinerCollider;
|
||||
public BoxCollider ConfinerCollider => _confinerCollider;
|
||||
public CameraLensConfigSO LensConfig => _lensConfig;
|
||||
public float LastSyncFOV => _lastSyncFOV;
|
||||
public CameraBlendProfileSO BlendProfile => _blendProfile;
|
||||
@@ -129,12 +155,17 @@ namespace BaseGames.Camera
|
||||
public float LookaheadTime => _lookaheadTime;
|
||||
public float LookaheadSmoothing => _lookaheadSmoothing;
|
||||
public bool DisableFallBias => _disableFallBias;
|
||||
public bool OverrideFacingBias => _overrideFacingBias;
|
||||
public float FacingBiasOverride => _facingBiasOverride;
|
||||
public bool LockHorizontal => _lockHorizontal;
|
||||
public bool LockVertical => _lockVertical;
|
||||
public float DampingDown => _dampingDown;
|
||||
public float DampingUp => _dampingUp;
|
||||
public float LensSize => _lensSize;
|
||||
public float LensSizeDuration => _lensSizeDuration;
|
||||
public NoiseSettings NoiseProfile => _noiseProfile;
|
||||
public float NoiseAmplitude => _noiseAmplitude;
|
||||
public float NoiseFrequency => _noiseFrequency;
|
||||
|
||||
/// <summary>
|
||||
/// 摄像机到场景平面的有效深度(用于透视视口换算)。
|
||||
@@ -150,6 +181,16 @@ namespace BaseGames.Camera
|
||||
}
|
||||
}
|
||||
|
||||
// ── Lifecycle ────────────────────────────────────────────────────────
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// 确保专属 VCam 启动时 Priority=0,无论 Inspector 保存了什么值。
|
||||
// CameraStateController.ActivateDedicated 会在进入区域时将其提升到 DedicatedPriority。
|
||||
if (_dedicatedCamera != null)
|
||||
_dedicatedCamera.Priority = 0;
|
||||
}
|
||||
|
||||
// ── Gizmo ────────────────────────────────────────────────────────────
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
|
||||
@@ -34,10 +34,18 @@ namespace BaseGames.Camera
|
||||
{
|
||||
if (stage != CinemachineCore.Stage.Body) return;
|
||||
|
||||
var pos = state.RawPosition;
|
||||
if (LockX) pos.x = LockedX;
|
||||
if (LockY) pos.y = LockedY;
|
||||
state.RawPosition = pos;
|
||||
// 通过覆写 PositionCorrection 而非 RawPosition 来锁定轴向。
|
||||
// 最终相机位置 = RawPosition + PositionCorrection。
|
||||
// 只修改 PositionCorrection 可确保:
|
||||
// - 锁定轴:final = RawPos + (LockedVal - RawPos) = LockedVal,无论 Confiner 之前写入了什么修正量
|
||||
// - 非锁定轴:保留 CinemachineConfiner3D 已计算好的 PositionCorrection,限位正常生效
|
||||
// 若改为修改 RawPosition,在 Confiner 之后运行时会导致
|
||||
// final = LockedVal + ConfinementCorrection ≠ LockedVal,使 Confiner 修正量错误叠加。
|
||||
var rawPos = state.RawPosition;
|
||||
var correction = state.PositionCorrection;
|
||||
if (LockX) correction.x = LockedX - rawPos.x;
|
||||
if (LockY) correction.y = LockedY - rawPos.y;
|
||||
state.PositionCorrection = correction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
164
Assets/_Game/Scripts/Camera/CameraFacingBiasExtension.cs
Normal file
164
Assets/_Game/Scripts/Camera/CameraFacingBiasExtension.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
using UnityEngine;
|
||||
using Unity.Cinemachine;
|
||||
|
||||
namespace BaseGames.Camera
|
||||
{
|
||||
/// <summary>
|
||||
/// 方向感知水平偏置扩展(Facing-aware Horizontal Bias)。
|
||||
///
|
||||
/// 持续估算跟随目标的水平移动速度,动态将相机 X 轴向玩家前进方向偏移,
|
||||
/// 使玩家始终出现在画面偏后侧,前方预留更多可见视野。
|
||||
///
|
||||
/// <para>方向逻辑:</para>
|
||||
/// <list type="bullet">
|
||||
/// <item>向右移动 → 相机向右偏置 → 玩家出现在屏幕左侧 → 右方视野增加。</item>
|
||||
/// <item>向左移动 → 相机向左偏置 → 玩家出现在屏幕右侧 → 左方视野增加。</item>
|
||||
/// <item>静止或速度低于阈值 → 保持当前偏置方向,不回中(避免静止时镜头漂回)。</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>
|
||||
/// 注意:本扩展通过直接修改 <c>state.RawPosition.x</c> 在 Body 阶段之后叠加偏置,
|
||||
/// 不与 <see cref="CinemachinePositionComposer"/> 的 ScreenPosition.x 冲突;
|
||||
/// 但偏置量会占用 Confiner 的裁剪空间,在较窄区域中建议通过
|
||||
/// <see cref="CameraArea._facingBiasOverride"/> 设为较小值或 0 以防止越界。
|
||||
/// </para>
|
||||
///
|
||||
/// <para>挂载顺序(扩展执行顺序由组件顺序决定):</para>
|
||||
/// <list type="number">
|
||||
/// <item><see cref="CameraAsymmetricDampingExtension"/> — Y 轴非对称阻尼</item>
|
||||
/// <item><see cref="CameraFallBiasExtension"/> — 下坠 Y 偏置</item>
|
||||
/// <item><b>本扩展(CameraFacingBiasExtension)</b> — X 轴方向偏置</item>
|
||||
/// <item><c>CinemachineConfiner3D</c> — 最后裁剪至限位边界</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[AddComponentMenu("Cinemachine/Extensions/Camera Facing Bias")]
|
||||
[DisallowMultipleComponent]
|
||||
public class CameraFacingBiasExtension : CinemachineExtension
|
||||
{
|
||||
[Tooltip("相机沿面朝方向的偏移量(世界单位)。\n" +
|
||||
"玩家向右时相机向右偏此值,使玩家出现在画面左侧,前方(右侧)视野增加;反之亦然。\n" +
|
||||
"0 = 禁用。推荐 1.5~2.5,视房间宽度和视口大小而定。")]
|
||||
[Range(0f, 6f)]
|
||||
[SerializeField] private float _facingBias = 2f;
|
||||
|
||||
[Tooltip("偏置方向切换时的过渡时长(秒)。越大越平滑但延迟越长。\n" +
|
||||
"推荐 0.3~0.5。")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float _transitionTime = 0.4f;
|
||||
|
||||
[Tooltip("触发方向切换所需的最小水平速度(世界单位/秒)。\n" +
|
||||
"低于此值时不更新目标偏置,防止静止时微小输入造成镜头反复横跳。\n" +
|
||||
"推荐 1.0~2.0。")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float _directionThreshold = 1.5f;
|
||||
|
||||
[Tooltip("水平速度估算的平滑强度(越大响应越快)。推荐 6~10。")]
|
||||
[Min(0.1f)]
|
||||
[SerializeField] private float _velocitySmoothing = 8f;
|
||||
|
||||
// ── 内部状态 ──────────────────────────────────────────────────────────
|
||||
private float _currentBiasX; // 当前实际偏置(世界单位,已指数平滑)
|
||||
private float _targetBiasX; // 当前目标偏置
|
||||
private float _estimatedVX; // 平滑后的水平速度估算(世界单位/秒)
|
||||
private float _lastFollowX;
|
||||
private bool _initialized;
|
||||
private int _externalFacing = 0; // 0 = 未设置(回退到速度估算);+1 = 朝右;-1 = 朝左
|
||||
// ── 公开 API ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 供 <see cref="CameraStateController"/> 运行时按区域写入偏置量。
|
||||
/// 0 = 禁用此区域的方向偏置。
|
||||
/// </summary>
|
||||
public float FacingBias { get => _facingBias; set => _facingBias = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 由 PlayerController 在精灵翻转时直接注入面朝方向,使偏置立即响应角色转向。
|
||||
/// direction: +1 = 朝右,-1 = 朝左,0 = 清除外部输入(回退到速度估算)。
|
||||
/// 外部常量优先于速度估算,即使玩家缓慢行走或原地站立翻转,相机也能立即更新。
|
||||
/// </summary>
|
||||
public void SetExternalFacing(int direction)
|
||||
{
|
||||
_externalFacing = Mathf.Clamp(direction, -1, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置内部状态。在相机硬切(instantCut)时由 CameraStateController 调用,
|
||||
/// 防止旧房间的方向偏置和速度历史带入新房间。
|
||||
/// </summary>
|
||||
public void ResetState()
|
||||
{
|
||||
_initialized = false;
|
||||
_estimatedVX = 0f;
|
||||
// 硬切时直接吸附到当前目标值,不保留旧房间的过渡惯性
|
||||
_currentBiasX = _targetBiasX;
|
||||
}
|
||||
|
||||
// ── Cinemachine Extension ─────────────────────────────────────────────
|
||||
|
||||
protected override void PostPipelineStageCallback(
|
||||
CinemachineVirtualCameraBase vcam,
|
||||
CinemachineCore.Stage stage,
|
||||
ref CameraState state,
|
||||
float deltaTime)
|
||||
{
|
||||
// 在 Body 阶段之后(Composer 已计算出位置)叠加水平偏置
|
||||
if (stage != CinemachineCore.Stage.Body) return;
|
||||
|
||||
// 编辑器预览 / 初始帧:重置平滑器
|
||||
if (deltaTime <= 0f)
|
||||
{
|
||||
_initialized = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 偏置量为 0 时跳过,不修改位置
|
||||
if (_facingBias <= 0f) return;
|
||||
|
||||
Transform follow = vcam.Follow;
|
||||
if (follow == null) return;
|
||||
|
||||
// ── 初始化:第一帧直接吸附,不做过渡动画 ────────────────────────
|
||||
if (!_initialized)
|
||||
{
|
||||
_lastFollowX = follow.position.x;
|
||||
_initialized = true;
|
||||
_currentBiasX = _targetBiasX;
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 估算水平速度(帧率无关指数平滑)────────────────────────────
|
||||
float rawVX = (follow.position.x - _lastFollowX) / deltaTime;
|
||||
_lastFollowX = follow.position.x;
|
||||
_estimatedVX = Mathf.Lerp(_estimatedVX, rawVX, 1f - Mathf.Exp(-deltaTime * _velocitySmoothing));
|
||||
|
||||
// ── 方向判定(带死区,静止时保持当前目标方向)──────────────────
|
||||
// 优先使用外部注入朝向(PlayerController.OnFlip 传入);
|
||||
// 未设置时回退到速度估算(却保留静止不回中行为)。
|
||||
if (_externalFacing != 0)
|
||||
{
|
||||
_targetBiasX = _externalFacing * _facingBias;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_estimatedVX > _directionThreshold)
|
||||
_targetBiasX = +_facingBias; // 向右 → 相机右偏 → 玩家在左,前方(右侧)视野增
|
||||
else if (_estimatedVX < -_directionThreshold)
|
||||
_targetBiasX = -_facingBias; // 向左 → 相机左偏 → 玩家在右,前方(左侧)视野增
|
||||
// else: 速度不足,保持 _targetBiasX 不变(不回中)
|
||||
}
|
||||
|
||||
// ── 指数平滑过渡 ─────────────────────────────────────────────────
|
||||
float t = _transitionTime > 0f
|
||||
? 1f - Mathf.Exp(-deltaTime / _transitionTime)
|
||||
: 1f;
|
||||
_currentBiasX = Mathf.Lerp(_currentBiasX, _targetBiasX, t);
|
||||
|
||||
// ── 叠加到 Composer 已计算的位置上 ──────────────────────────────
|
||||
// 注意:此偏置在 CinemachineConfiner3D 之前施加,
|
||||
// Confiner 会在之后将结果裁剪回限位边界内。
|
||||
var pos = state.RawPosition;
|
||||
pos.x += _currentBiasX;
|
||||
state.RawPosition = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38830e1649a9eb548b20540a737bdf09
|
||||
guid: db8aa13bc9ef2124d8512b64b173d768
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -15,7 +15,7 @@ namespace BaseGames.Camera
|
||||
/// <list type="number">
|
||||
/// <item><see cref="CameraAsymmetricDampingExtension"/> — 先对 Y 轴做非对称阻尼平滑;</item>
|
||||
/// <item><b>本扩展(CameraFallBiasExtension)</b> — 将偏置叠加到平滑后的 Y 上;</item>
|
||||
/// <item><c>CinemachineConfiner2D</c> — 最后将偏置后的位置裁剪回限位边界内。</item>
|
||||
/// <item><c>CinemachineConfiner3D</c> — 最后将偏置后的位置裁剪回限位边界内。</item>
|
||||
/// </list>
|
||||
/// <para>如果顺序错误(本扩展在 Confiner 之后),偏置会导致相机超出限位边界且不被修正。</para>
|
||||
/// </summary>
|
||||
@@ -98,10 +98,10 @@ namespace BaseGames.Camera
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 估算玩家 Y 轴速度(负值 = 下落)────────────────────────────
|
||||
// ── 估算玩家 Y 轴速度(帧率无关指数平滑)─────────────────────
|
||||
float rawVY = (follow.position.y - _lastFollowY) / deltaTime;
|
||||
_lastFollowY = follow.position.y;
|
||||
_smoothedVY = Mathf.Lerp(_smoothedVY, rawVY, deltaTime * 10f);
|
||||
_smoothedVY = Mathf.Lerp(_smoothedVY, rawVY, 1f - Mathf.Exp(-deltaTime * 10f));
|
||||
|
||||
bool isFalling = _smoothedVY < -_fallSpeedThreshold;
|
||||
|
||||
@@ -115,7 +115,7 @@ namespace BaseGames.Camera
|
||||
// 超过 activationDelay 后线性增加偏置;0.4s 达到最大
|
||||
float effectiveMax = _configuredMaxShift >= 0f ? _configuredMaxShift : _maxShift;
|
||||
float ratio = Mathf.Clamp01((_fallTimer - _activationDelay) / 0.4f);
|
||||
float targetShift = -effectiveMax * ratio; // 负就:相机向下
|
||||
float targetShift = -effectiveMax * ratio; // 负值:相机向下
|
||||
|
||||
// 使用指数衰减公式(帧率无关)替代 Lerp*deltaTime
|
||||
float dampingTime = targetShift < _currentShift
|
||||
|
||||
@@ -3,12 +3,10 @@ using UnityEngine;
|
||||
namespace BaseGames.Camera
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局相机镜头配置。
|
||||
/// 相机镜头配置。
|
||||
///
|
||||
/// 作为 <see cref="CameraStateController"/> 和各 <see cref="CameraArea"/> 之间的
|
||||
/// 单一参数来源:
|
||||
/// - Persistent 场景的 <see cref="CameraStateController"/> 在 Awake 时将
|
||||
/// <see cref="fieldOfView"/> 写入两台全局 VCam 的 Lens。
|
||||
/// - Room 场景的 <see cref="CameraArea"/> 引用同一 SO,编辑器工具在计算限位多边形
|
||||
/// 时直接读取,无需依赖 Persistent 场景是否已加载。
|
||||
///
|
||||
@@ -21,15 +19,14 @@ namespace BaseGames.Camera
|
||||
[CreateAssetMenu(menuName = "BaseGames/Camera/Lens Config", fileName = "CameraLensConfig")]
|
||||
public class CameraLensConfigSO : ScriptableObject
|
||||
{
|
||||
[Tooltip("全局虚拟相机的垂直 FOV(度)。\n" +
|
||||
"修改此值后,编辑器会自动对所有已打开场景中的 CameraArea 重新同步限位多边形。\n" +
|
||||
"运行时由 CameraStateController 在 Awake 时应用到全局 VCam。")]
|
||||
[Tooltip("虚拟相机的垂直 FOV(度)。\n" +
|
||||
"修改此値后,编辑器会自动对所有已打开场景中的 CameraArea 重新同步限位多边形。")]
|
||||
[Range(1f, 179f)]
|
||||
public float fieldOfView = 60f;
|
||||
|
||||
[Tooltip("摄像机到场景平面(Z = 0)的垂直距离(世界单位)。\n" +
|
||||
"与 fieldOfView 共同决定透视相机的视口尺寸,\n" +
|
||||
"用于将可视区域(VisibleBounds)换算为 CinemachineConfiner2D 限位多边形。\n" +
|
||||
"用于将可视区域(VisibleBounds)换算为 CinemachineConfiner3D 限位体积。\n" +
|
||||
"推荐与 Persistent 场景中相机 Transform 的 |Z| 保持一致(通常为 10)。\n" +
|
||||
"CameraArea._cameraDepth > 0 时以区域专有值优先覆盖此全局值。")]
|
||||
[Min(0.1f)]
|
||||
|
||||
@@ -150,7 +150,7 @@ namespace BaseGames.Camera
|
||||
if (dt > 0f)
|
||||
{
|
||||
float rawSpeed = (_baseTarget.position - _lastBasePosition).magnitude / dt;
|
||||
_estimatedSpeed = Mathf.Lerp(_estimatedSpeed, rawSpeed, dt * 8f);
|
||||
_estimatedSpeed = Mathf.Lerp(_estimatedSpeed, rawSpeed, 1f - Mathf.Exp(-dt * 8f));
|
||||
}
|
||||
_lastBasePosition = _baseTarget.position;
|
||||
|
||||
@@ -170,7 +170,7 @@ namespace BaseGames.Camera
|
||||
}
|
||||
|
||||
float speedV = Mathf.Abs(_targetOffsetY) < 0.01f ? _resetSpeedV : _lookSpeedV;
|
||||
_currentOffsetY = Mathf.Lerp(_currentOffsetY, _targetOffsetY, dt * speedV);
|
||||
_currentOffsetY = Mathf.Lerp(_currentOffsetY, _targetOffsetY, 1f - Mathf.Exp(-dt * speedV));
|
||||
|
||||
// ── 水平窥视 ──────────────────────────────────────────────────────
|
||||
if (withinGate && Mathf.Abs(_inputX) > 0.5f)
|
||||
@@ -186,7 +186,7 @@ namespace BaseGames.Camera
|
||||
}
|
||||
|
||||
float speedH = Mathf.Abs(_targetOffsetX) < 0.01f ? _resetSpeedH : _lookSpeedH;
|
||||
_currentOffsetX = Mathf.Lerp(_currentOffsetX, _targetOffsetX, dt * speedH);
|
||||
_currentOffsetX = Mathf.Lerp(_currentOffsetX, _targetOffsetX, 1f - Mathf.Exp(-dt * speedH));
|
||||
|
||||
// ── 虚拟目标 = 玩家位置 + 双轴偏移 ────────────────────────────────
|
||||
_virtualTargetTransform.position = _baseTarget.position
|
||||
|
||||
114
Assets/_Game/Scripts/Camera/CameraPixelSnapper.cs
Normal file
114
Assets/_Game/Scripts/Camera/CameraPixelSnapper.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using UnityEngine;
|
||||
using Unity.Cinemachine;
|
||||
|
||||
namespace BaseGames.Camera
|
||||
{
|
||||
/// <summary>
|
||||
/// 相机像素对齐(Pixel-perfect Snapping)。
|
||||
///
|
||||
/// 每帧在 CinemachineBrain LateUpdate 完成后,将 Unity 主相机的世界坐标
|
||||
/// 四舍五入到最近的像素网格,消除像素艺术精灵的亚像素抖动(sub-pixel jitter)。
|
||||
///
|
||||
/// <para>
|
||||
/// 挂载位置:Persistent 场景中与 <see cref="CinemachineBrain"/> 同一 GameObject([Camera] 节点)。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 执行顺序:+1000,确保在 CinemachineBrain(默认 -1000)的 LateUpdate 之后运行。
|
||||
/// 直接修改 Unity Camera 的 <c>transform.position</c>,不影响 Cinemachine 内部状态。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 混合进行中时可选择暂停对齐(<see cref="_disableDuringBlend"/> = true),
|
||||
/// 避免两台 VCam 插值结果在整数像素间跳变,产生阶梯感。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(1000)]
|
||||
[AddComponentMenu("BaseGames/Camera/Camera Pixel Snapper")]
|
||||
[RequireComponent(typeof(UnityEngine.Camera))]
|
||||
public class CameraPixelSnapper : MonoBehaviour
|
||||
{
|
||||
[Tooltip("每世界单位的像素数(PPU)。须与项目精灵的 Pixels Per Unit 设置一致。\n" +
|
||||
"0 = 禁用像素对齐。常用值:16(粗像素)、32(标准)、100(高分辨率)。")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float _pixelsPerUnit = 16f;
|
||||
|
||||
[Tooltip("相机混合动画(Blend)进行中时暂停像素对齐,待混合结束后恢复。\n" +
|
||||
"开启时混合动画更平滑(无阶梯感);关闭时混合期间也精确对齐但可能有轻微跳帧。\n" +
|
||||
"推荐保持开启。")]
|
||||
[SerializeField] private bool _disableDuringBlend = true;
|
||||
|
||||
[SerializeField] private CinemachineBrain _brain;
|
||||
|
||||
private UnityEngine.Camera _camera;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
_brain = GetComponent<CinemachineBrain>();
|
||||
_camera = GetComponent<UnityEngine.Camera>();
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_camera = GetComponent<UnityEngine.Camera>();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (_pixelsPerUnit <= 0f || _camera == null) return;
|
||||
|
||||
// 混合进行中时跳过,避免插值位置在像素网格间产生阶梯感
|
||||
if (_disableDuringBlend && _brain != null && _brain.IsBlending) return;
|
||||
|
||||
float ppu = _pixelsPerUnit;
|
||||
Vector3 pos = _camera.transform.position;
|
||||
pos.x = Mathf.Round(pos.x * ppu) / ppu;
|
||||
pos.y = Mathf.Round(pos.y * ppu) / ppu;
|
||||
// Z 轴保持不变(透视深度不需要对齐)
|
||||
|
||||
// 像素取整可能将相机推出 Confiner 边界(最多 0.5/PPU 的微小超出)。
|
||||
// 对取整后的位置施加限位矩形钳制,确保不超出当前激活区域的 Confiner 边界。
|
||||
if (TryGetActiveConfinerBounds(out Bounds confinerBounds))
|
||||
{
|
||||
pos.x = Mathf.Clamp(pos.x, confinerBounds.min.x, confinerBounds.max.x);
|
||||
pos.y = Mathf.Clamp(pos.y, confinerBounds.min.y, confinerBounds.max.y);
|
||||
}
|
||||
|
||||
_camera.transform.position = pos;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前活跃 VCam 的 CinemachineConfiner3D 边界盒(世界空间 AABB)。
|
||||
/// 用于在像素取整后将相机钳制回限位区域内。
|
||||
/// </summary>
|
||||
private bool TryGetActiveConfinerBounds(out Bounds bounds)
|
||||
{
|
||||
bounds = default;
|
||||
if (_brain == null) return false;
|
||||
var vcam = _brain.ActiveVirtualCamera as CinemachineCamera;
|
||||
if (vcam == null) return false;
|
||||
var confiner = vcam.GetComponent<CinemachineConfiner3D>();
|
||||
if (confiner == null || !confiner.IsValid) return false;
|
||||
bounds = confiner.BoundingVolume.bounds;
|
||||
return true;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>编辑器模式下实时预览对齐效果。</summary>
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (_pixelsPerUnit <= 0f || _camera == null) return;
|
||||
float ppu = _pixelsPerUnit;
|
||||
float cellW = 1f / ppu;
|
||||
// 在 Scene 视图中围绕相机位置绘制 5×5 像素网格示意(仅辅助调试用)
|
||||
Gizmos.color = new Color(0.6f, 1f, 0.6f, 0.4f);
|
||||
Vector3 origin = _camera.transform.position;
|
||||
origin.x = Mathf.Floor(origin.x / cellW) * cellW;
|
||||
origin.y = Mathf.Floor(origin.y / cellW) * cellW;
|
||||
for (int ix = -2; ix <= 3; ix++)
|
||||
for (int iy = -2; iy <= 3; iy++)
|
||||
Gizmos.DrawWireCube(
|
||||
new Vector3(origin.x + ix * cellW, origin.y + iy * cellW, 0f),
|
||||
new Vector3(cellW, cellW, 0.01f));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Camera/CameraPixelSnapper.cs.meta
Normal file
11
Assets/_Game/Scripts/Camera/CameraPixelSnapper.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c2869ceeab4c184180a029bfc710cd0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
@@ -10,14 +11,9 @@ namespace BaseGames.Camera
|
||||
/// <summary>
|
||||
/// 相机状态单例控制器。须放置在 Persistent 场景中。
|
||||
///
|
||||
/// 支持两种相机切换模式:
|
||||
/// 1. 全局双 VCam 模式(推荐):<see cref="SwitchArea"/>
|
||||
/// 两台全局 CinemachineCamera(<c>_vcamA</c> / <c>_vcamB</c>)交替承接各区域,
|
||||
/// 通过优先级 ping-pong 触发 Cinemachine 混合过渡。场景内无需每个区域都放置 VCam。
|
||||
///
|
||||
/// 2. 专有 VCam 模式:<see cref="SwitchArea"/>(区域含 dedicatedCamera 时自动使用)
|
||||
/// 激活该区域专属的 CinemachineCamera(优先级 > 全局 VCam),
|
||||
/// 适用于需要独特相机参数的特殊区域。
|
||||
/// 每个 <see cref="CameraArea"/> 均拥有自己专属的 <c>DedicatedCamera</c>,
|
||||
/// 进入区域时调用 <see cref="ActivateDedicated"/> 激活对应 VCam,
|
||||
/// Cinemachine Brain 自动处理混合过渡。
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-100)]
|
||||
public class CameraStateController : MonoBehaviour, ICameraService
|
||||
@@ -27,43 +23,28 @@ namespace BaseGames.Camera
|
||||
[SerializeField] private CinemachineImpulseSource _impulseSource;
|
||||
[SerializeField] private CameraLookSystem _lookSystem;
|
||||
|
||||
[Header("全局双 VCam(Persistent 场景中放置两台通用虚拟相机)")]
|
||||
[Tooltip("两台 VCam 交替承接各相机区域,通过优先级 ping-pong 触发混合过渡。\n" +
|
||||
"须各自挂载 CinemachineCamera + CinemachineConfiner2D;\n" +
|
||||
"Follow 指向 Player/CameraFollowTarget(或运行时调用 SetFollowTarget 赋值)。")]
|
||||
[SerializeField] private CinemachineCamera _vcamA;
|
||||
[SerializeField] private CinemachineCamera _vcamB;
|
||||
|
||||
[Tooltip("全局 VCam 激活时的优先级。专有 VCam 的 _dedicatedPriority 须高于此值。")]
|
||||
[SerializeField] private int _globalActivePriority = 10;
|
||||
|
||||
[Tooltip("待机 VCam 的优先级。\n" +
|
||||
"Cinemachine 3.x 中 Priority = 0 的 VCam 不会被 Brain 选中,导致主相机停止跟随。\n" +
|
||||
"必须 > 0 且 < _globalActivePriority,确保 Brain 始终有可用 VCam,\n" +
|
||||
"同时切换时两台 VCam 均在 Brain 视野内以完成正确的混合过渡。")]
|
||||
[SerializeField] private int _standbyPriority = 1;
|
||||
|
||||
[Header("默认混合配置")]
|
||||
[SerializeField] private CameraBlendProfileSO _defaultBlendProfile;
|
||||
|
||||
[Header("镜头配置")]
|
||||
[Tooltip("全局镜头参数 SO。Awake 时将 fieldOfView 应用到 _vcamA / _vcamB。\n" +
|
||||
"与各 CameraArea 引用同一资产,确保 FOV 参数一致。")]
|
||||
[Tooltip("相机镜头参数 SO,提供 FOV 与相机深度。\n" +
|
||||
"与各 CameraArea 引用同一资产,确保 SetLensSize 换算结果与 VCam 配置一致。")]
|
||||
[SerializeField] private CameraLensConfigSO _lensConfig;
|
||||
|
||||
[Header("玩家跟随")]
|
||||
[Tooltip("PlayerController 生成时广播的事件频道(EVT_PlayerSpawned)。\n" +
|
||||
"收到后自动查找 CameraFollowTarget 子节点并赋值给两台全局 VCam 的 Follow。")]
|
||||
"收到后自动查找 CameraFollowTarget 子节点并作为 VCam Follow 赋值。")]
|
||||
[SerializeField] private TransformEventChannelSO _onPlayerSpawned;
|
||||
|
||||
// ── 状态 ──────────────────────────────────────────────────────────────
|
||||
private int _activeSlot = -1; // -1 = 未初始化;0 = A;1 = B
|
||||
private CameraArea _roomBaselineArea; // SwitchArea(priority=0) 写入的房间基线,不被触发事件删除
|
||||
private readonly List<(CameraArea area, int priority)> _activeZones = new(); // 玩家当前所在的触发区域集合(priority>0)
|
||||
private CameraArea _currentArea;
|
||||
private CinemachineCamera _activeDedicatedCam;
|
||||
private CinemachineConfiner2D _confinerA;
|
||||
private CinemachineConfiner2D _confinerB;
|
||||
private CinemachineCamera _cutsceneCamera; // 过场模式专用高优先级 VCam
|
||||
private const int CutscenePriority = 100; // 高于专有区域 VCam(默认 20)
|
||||
private int _lastExternalFacing = 0; // 最近一次 SetPlayerFacing 的值,用于新激活 VCam 的初始化
|
||||
private Transform _currentFollowTarget; // 最后一次 SetFollowTarget 设置的目标,激活 VCam 时自动同步
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
@@ -74,17 +55,14 @@ namespace BaseGames.Camera
|
||||
if (ServiceLocator.GetOrDefault<ICameraService>() != null) { Destroy(gameObject); return; }
|
||||
ServiceLocator.Register<ICameraService>(this);
|
||||
|
||||
// 缓存 Confiner 引用
|
||||
if (_vcamA != null) _confinerA = _vcamA.GetComponent<CinemachineConfiner2D>();
|
||||
if (_vcamB != null) _confinerB = _vcamB.GetComponent<CinemachineConfiner2D>();
|
||||
|
||||
// 初始两台 VCam 均处于待机优先级(> 0)
|
||||
// Cinemachine 3.x 中 Priority = 0 的 VCam 不被 Brain 选中,主相机会停止运动
|
||||
if (_vcamA != null) _vcamA.Priority = _standbyPriority;
|
||||
if (_vcamB != null) _vcamB.Priority = _standbyPriority;
|
||||
|
||||
// 将 SO 中的 FOV 应用到两台全局 VCam
|
||||
ApplyLensConfig();
|
||||
// 重置运行时状态,防止禁用 Domain Reload 时上一次 Play Mode 的数据残留。
|
||||
// 非序列化字段在 Domain Reload 禁用时不会自动清零。
|
||||
_roomBaselineArea = null;
|
||||
_activeZones.Clear();
|
||||
_currentArea = null;
|
||||
_activeDedicatedCam = null;
|
||||
_lastExternalFacing = 0;
|
||||
_currentFollowTarget = null;
|
||||
|
||||
// 订阅 PlayerSpawned 事件,运行时自动为 VCam 赋值 Follow
|
||||
_onPlayerSpawned?.Subscribe(OnPlayerSpawned).AddTo(_subs);
|
||||
@@ -103,35 +81,18 @@ namespace BaseGames.Camera
|
||||
SetFollowTarget(follow);
|
||||
}
|
||||
|
||||
private void ApplyLensConfig()
|
||||
private void Start()
|
||||
{
|
||||
if (_lensConfig == null) return;
|
||||
float fov = _lensConfig.fieldOfView;
|
||||
float depth = _lensConfig.cameraDepth;
|
||||
ApplyLensToVcam(_vcamA, fov, depth);
|
||||
ApplyLensToVcam(_vcamB, fov, depth);
|
||||
}
|
||||
|
||||
private static void ApplyLensToVcam(CinemachineCamera vcam, float fov, float depth)
|
||||
// 场景启动时扫描全部 VCam,提前暴露组件顺序错误,
|
||||
// 无需等待各区域被激活才触发检测。
|
||||
var allVCams = FindObjectsByType<CinemachineCamera>(FindObjectsSortMode.None);
|
||||
foreach (var vcam in allVCams)
|
||||
{
|
||||
if (vcam == null) return;
|
||||
var lens = vcam.Lens;
|
||||
lens.FieldOfView = fov;
|
||||
vcam.Lens = lens;
|
||||
// CinemachinePositionComposer.CameraDistance 是运行时真正控制 Z 距离的属性,
|
||||
// 必须同步,否则 Transform Z 被 Cinemachine Pipeline 覆盖
|
||||
var composer = vcam.GetComponent<CinemachinePositionComposer>();
|
||||
if (composer != null)
|
||||
composer.CameraDistance = depth;
|
||||
// 同步 Transform Z,保证编辑器预览与运行时一致
|
||||
var pos = vcam.transform.position;
|
||||
pos.z = -depth;
|
||||
vcam.transform.position = pos;
|
||||
var confiner = vcam.GetComponent<CinemachineConfiner3D>();
|
||||
if (confiner != null)
|
||||
ValidateVCamExtensionOrder(vcam, confiner);
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate() => ApplyLensConfig();
|
||||
#endif
|
||||
|
||||
// ── 公开 API ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -156,7 +117,7 @@ namespace BaseGames.Camera
|
||||
_activeZones.RemoveAll(e => e.area == area);
|
||||
_activeZones.Add((area, priority));
|
||||
|
||||
// 仅当此区域是当前最优且尚未激活时才切换,避免不必要的 ping-pong
|
||||
// 仅当此区域是当前最优且尚未激活时才切换
|
||||
CameraArea best = GetEffectiveArea();
|
||||
if (best == area && area != _currentArea)
|
||||
ActivateArea(area, instantCut);
|
||||
@@ -172,7 +133,8 @@ namespace BaseGames.Camera
|
||||
|
||||
bool wasActive = releasedArea == _currentArea;
|
||||
int removed = _activeZones.RemoveAll(e => e.area == releasedArea);
|
||||
if (removed == 0) return;
|
||||
// 若区域本就不在栈中,且又不是当前激活区,则无需任何操作
|
||||
if (removed == 0 && !wasActive) return;
|
||||
|
||||
if (!wasActive) return;
|
||||
|
||||
@@ -209,10 +171,10 @@ namespace BaseGames.Camera
|
||||
Time = 0f,
|
||||
};
|
||||
// 重置窥视偏移,避免旧房间的窥视状态残留
|
||||
_lookSystem?.ResetOffsets(snap: true); // 重置所有 VCam 扩展的内部状态,防止旧房间的速度/阻尼估算带入新房间
|
||||
ResetVCamExtensions(_vcamA);
|
||||
ResetVCamExtensions(_vcamB);
|
||||
if (area.HasDedicated) ResetVCamExtensions(area.DedicatedCamera); }
|
||||
_lookSystem?.ResetOffsets(snap: true);
|
||||
// 重置专属 VCam 扩展的内部状态,防止旧房间的速度/阻尼估算带入新房间
|
||||
if (area.HasDedicated) ResetVCamExtensions(area.DedicatedCamera);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyBlendProfile(area.BlendProfile ?? _defaultBlendProfile);
|
||||
@@ -224,12 +186,12 @@ namespace BaseGames.Camera
|
||||
if (area.HasDedicated)
|
||||
ActivateDedicated(area);
|
||||
else
|
||||
ActivateGlobalSlot(area);
|
||||
Debug.LogError($"[CameraStateController] {area.name} 缺少专属 VCam!请通过 Camera Area Setup 工具为此区域创建 DedicatedCamera。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行时为全局双 VCam 设置跟随目标。
|
||||
/// 若存在 <see cref="CameraLookSystem"/>,VCam 跟随系统输出的虚拟目标(含窥视偏移)。
|
||||
/// 运行时设置跟随目标。
|
||||
/// 若存在 <see cref="CameraLookSystem"/>,VCam 跟随系统输出的虚拟目标(含窗斥偏移)。
|
||||
/// </summary>
|
||||
public void SetFollowTarget(Transform followTarget)
|
||||
{
|
||||
@@ -239,9 +201,8 @@ namespace BaseGames.Camera
|
||||
_lookSystem.SetBaseTarget(followTarget);
|
||||
actual = _lookSystem.VirtualTarget;
|
||||
}
|
||||
_currentFollowTarget = actual; // 缓存供后续激活 VCam 时同步
|
||||
if (_vcamA != null) _vcamA.Follow = actual;
|
||||
if (_vcamB != null) _vcamB.Follow = actual;
|
||||
_currentFollowTarget = actual;
|
||||
SyncFollowToVCam(_activeDedicatedCam); // 立即同步到当前活跃专有 VCam
|
||||
}
|
||||
|
||||
/// <summary>触发屏幕抖动。</summary>
|
||||
@@ -255,30 +216,72 @@ namespace BaseGames.Camera
|
||||
=> TriggerImpulse(Vector3.down * strength);
|
||||
|
||||
/// <summary>
|
||||
/// 平滑过渡正交相机尺寸。<paramref name="duration"/> = 0 时瞬间切换。
|
||||
/// 平滑过渡视野尺寸(可视半高,世界单位)。<paramref name="duration"/> = 0 时瞬间切换。
|
||||
/// 透视相机下自动换算为 FOV;语义等价于正交相机 OrthographicSize。
|
||||
/// 区域进入时由 <see cref="CameraArea"/> 自动调用;游戏代码也可直接调用。
|
||||
/// </summary>
|
||||
public void SetLensSize(float orthographicSize, float duration = 0f)
|
||||
public void SetLensSize(float visibleHalfHeight, float duration = 0f)
|
||||
{
|
||||
if (_lensCoroutine != null) StopCoroutine(_lensCoroutine);
|
||||
if (duration <= 0f) { ApplyLensSizeToAll(orthographicSize); return; }
|
||||
_lensCoroutine = StartCoroutine(LensSizeCo(orthographicSize, duration));
|
||||
if (duration <= 0f) { ApplyLensSizeToAll(visibleHalfHeight); return; }
|
||||
_lensCoroutine = StartCoroutine(LensSizeCo(visibleHalfHeight, duration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 进入过场模式,将指定 VCam 提升至最高优先级,Brain 自动混合到它。
|
||||
/// <para>
|
||||
/// 过场 VCam 由设计者在 Inspector 中预先配置(位置、Follow、LookAt、Lens 等);
|
||||
/// 此方法不强制覆写 Follow,保留 Inspector 配置不变。
|
||||
/// </para>
|
||||
/// <para>适用场景:Boss 登场固定镜头、对话拉近、剧情事件全景等。</para>
|
||||
/// </summary>
|
||||
public void EnterCutsceneMode(CinemachineCamera cutsceneCamera)
|
||||
{
|
||||
if (cutsceneCamera == null) return;
|
||||
if (_cutsceneCamera != null && _cutsceneCamera != cutsceneCamera)
|
||||
_cutsceneCamera.Priority = 0;
|
||||
_cutsceneCamera = cutsceneCamera;
|
||||
_cutsceneCamera.Priority = CutscenePriority;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 退出过场模式,撤销过场 VCam 的优先级,Brain 自动混合回当前区域相机。
|
||||
/// </summary>
|
||||
public void ExitCutsceneMode()
|
||||
{
|
||||
if (_cutsceneCamera == null) return;
|
||||
_cutsceneCamera.Priority = 0;
|
||||
_cutsceneCamera = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知相机系统玩家的面朝方向,转发至所有活跃 VCam 的方向偏置扩展。
|
||||
/// 由 PlayerController 在精灵翻转时调用:<c>ICameraService.SetPlayerFacing(翻转方向 ? +1 : -1)</c>。
|
||||
/// </summary>
|
||||
public void SetPlayerFacing(int direction)
|
||||
{
|
||||
_lastExternalFacing = direction;
|
||||
SetFacingOnVCam(_activeDedicatedCam, direction);
|
||||
}
|
||||
|
||||
private static void SetFacingOnVCam(CinemachineCamera vcam, int direction)
|
||||
=> vcam?.GetComponent<CameraFacingBiasExtension>()?.SetExternalFacing(direction);
|
||||
|
||||
private Coroutine _lensCoroutine;
|
||||
|
||||
private void ApplyLensSizeToAll(float size)
|
||||
{
|
||||
SetVcamLens(_vcamA, size);
|
||||
SetVcamLens(_vcamB, size);
|
||||
if (_activeDedicatedCam != null) SetVcamLens(_activeDedicatedCam, size);
|
||||
}
|
||||
|
||||
private static void SetVcamLens(CinemachineCamera vcam, float size)
|
||||
// size = 可见半高(世界单位),透视相机下等效于正交 OrthographicSize。
|
||||
// 换算公式:FOV = 2 * atan(size / depth)
|
||||
private void SetVcamLens(CinemachineCamera vcam, float size)
|
||||
{
|
||||
if (vcam == null) return;
|
||||
var lens = vcam.Lens;
|
||||
lens.OrthographicSize = size;
|
||||
float depth = _lensConfig != null ? _lensConfig.cameraDepth : 10f;
|
||||
lens.FieldOfView = 2f * Mathf.Atan(size / depth) * Mathf.Rad2Deg;
|
||||
vcam.Lens = lens;
|
||||
}
|
||||
|
||||
@@ -286,23 +289,24 @@ namespace BaseGames.Camera
|
||||
{
|
||||
CinemachineCamera active = GetActiveVcam();
|
||||
if (active == null) { _lensCoroutine = null; yield break; }
|
||||
float start = active.Lens.OrthographicSize;
|
||||
// 透视相机:从当前 FOV 反算等效可见半高,作为插值起点
|
||||
float depth = _lensConfig != null ? _lensConfig.cameraDepth : 10f;
|
||||
float start = depth * Mathf.Tan(active.Lens.FieldOfView * 0.5f * Mathf.Deg2Rad);
|
||||
float elapsed = 0f;
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
ApplyLensSizeToAll(Mathf.Lerp(start, target, elapsed / duration));
|
||||
// 使用平滑过渡曲线(ease-in-out),视野缩放手感更自然。
|
||||
// 线性插值会让镜头拉远感觉机械;平滑步多出平稳的起笔和收尾弹性。
|
||||
float t = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(elapsed / duration));
|
||||
ApplyLensSizeToAll(Mathf.Lerp(start, target, t));
|
||||
yield return null;
|
||||
}
|
||||
ApplyLensSizeToAll(target);
|
||||
_lensCoroutine = null;
|
||||
}
|
||||
|
||||
private CinemachineCamera GetActiveVcam()
|
||||
{
|
||||
if (_activeDedicatedCam != null) return _activeDedicatedCam;
|
||||
return _activeSlot == 0 ? _vcamA : (_vcamB != null ? _vcamB : _vcamA);
|
||||
}
|
||||
private CinemachineCamera GetActiveVcam() => _activeDedicatedCam;
|
||||
|
||||
// ── 内部方法 ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -315,64 +319,89 @@ namespace BaseGames.Camera
|
||||
|
||||
_activeDedicatedCam = area.DedicatedCamera;
|
||||
_activeDedicatedCam.Priority = area.DedicatedPriority;
|
||||
SyncFollowToVCam(_activeDedicatedCam); // 确保专有 VCam 的 Follow 指向当前跟随目标
|
||||
SetFacingOnVCam(_activeDedicatedCam, _lastExternalFacing); // 应用当前玩家朝向
|
||||
|
||||
// 应用 CameraArea 参数(Confiner、Composer、扩展组件等)
|
||||
var dedicatedConfiner = _activeDedicatedCam.GetComponent<CinemachineConfiner3D>();
|
||||
ValidateVCamExtensionOrder(_activeDedicatedCam, dedicatedConfiner);
|
||||
ConfigureSlot(_activeDedicatedCam, dedicatedConfiner, area);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用全局 VCam ping-pong 切换到新区域。
|
||||
/// 配置非活跃 VCam 的 Confiner → 提升其优先级 → 降低旧 VCam 优先级。
|
||||
/// Cinemachine Brain 检测到优先级变化后自动触发混合。
|
||||
/// 检查 VCam 上各扩展组件的挂载顺序是否正确。
|
||||
/// <list type="bullet">
|
||||
/// <item>FallBiasExtension / FacingBiasExtension 必须在 CinemachineConfiner3D 之前</item>
|
||||
/// <item>AxisLockExtension 必须在 CinemachineConfiner3D 之后</item>
|
||||
/// </list>
|
||||
/// 顺序错误时相机会在应用偏置后逃出限位区域,或轴锁被 Confiner 覆盖失效。
|
||||
/// </summary>
|
||||
private void ActivateGlobalSlot(CameraArea area)
|
||||
private static void ValidateVCamExtensionOrder(CinemachineCamera vcam, CinemachineConfiner3D confiner)
|
||||
{
|
||||
// 收回专有 VCam
|
||||
if (_activeDedicatedCam != null)
|
||||
{
|
||||
_activeDedicatedCam.Priority = 0;
|
||||
_activeDedicatedCam = null;
|
||||
}
|
||||
if (vcam == null) return;
|
||||
|
||||
bool noVCams = _vcamA == null && _vcamB == null;
|
||||
if (noVCams)
|
||||
if (confiner == null)
|
||||
{
|
||||
Debug.LogWarning("[CameraStateController] 全局 VCam A / B 均未绑定,无法切换相机区域。");
|
||||
Debug.LogWarning(
|
||||
$"[CameraStateController] VCam <b>{vcam.name}</b> 缺少 CinemachineConfiner3D 组件!" +
|
||||
"相机将不受任何限位约束,请通过 CameraAreaEditor 重新生成此 VCam 或手动添加。");
|
||||
return;
|
||||
}
|
||||
|
||||
// 首次调用:直接激活 VCamA(场景淡入阶段,无需混合动画)
|
||||
if (_activeSlot < 0)
|
||||
Component[] comps = vcam.GetComponents<Component>();
|
||||
int confinerIdx = -1;
|
||||
int fallBiasIdx = -1;
|
||||
int facingBiasIdx = -1;
|
||||
int axisLockIdx = -1;
|
||||
int asymDampIdx = -1;
|
||||
int adaptiveLahIdx = -1;
|
||||
|
||||
for (int i = 0; i < comps.Length; i++)
|
||||
{
|
||||
var cam = _vcamA ?? _vcamB;
|
||||
var confiner = _vcamA != null ? _confinerA : _confinerB;
|
||||
ConfigureSlot(cam, confiner, area);
|
||||
SyncFollowToVCam(cam);
|
||||
cam.Priority = _globalActivePriority;
|
||||
_activeSlot = _vcamA != null ? 0 : 1;
|
||||
return;
|
||||
switch (comps[i])
|
||||
{
|
||||
case CinemachineConfiner3D _: confinerIdx = i; break;
|
||||
case CameraFallBiasExtension _: fallBiasIdx = i; break;
|
||||
case CameraFacingBiasExtension _: facingBiasIdx = i; break;
|
||||
case CameraAxisLockExtension _: axisLockIdx = i; break;
|
||||
case CameraAsymmetricDampingExtension _: asymDampIdx = i; break;
|
||||
case CameraAdaptiveLookaheadExtension _: adaptiveLahIdx = i; break;
|
||||
}
|
||||
}
|
||||
|
||||
// 只有一台 VCam 时:直接重新配置,不做优先级 ping-pong
|
||||
// (之前的 null 保护令 inactiveCam == activeCam,导致先升后降为 0 自毁)
|
||||
if (_vcamA == null || _vcamB == null)
|
||||
{
|
||||
var cam = _vcamA ?? _vcamB;
|
||||
var confiner = _vcamA != null ? _confinerA : _confinerB;
|
||||
ConfigureSlot(cam, confiner, area);
|
||||
SyncFollowToVCam(cam);
|
||||
cam.Priority = _globalActivePriority; // 保持激活,不改变 _activeSlot
|
||||
return;
|
||||
}
|
||||
if (asymDampIdx >= 0 && asymDampIdx > confinerIdx)
|
||||
Debug.LogError(
|
||||
$"[CameraStateController] VCam <b>{vcam.name}</b>:" +
|
||||
"CameraAsymmetricDampingExtension 必须在 CinemachineConfiner3D 之前!" +
|
||||
"当前顺序导致Y轴阻尼平滑值将相机推出限位区域而不被重新裁剪。" +
|
||||
"请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。");
|
||||
|
||||
// 双 VCam ping-pong:配置非活跃槽 → 升级其优先级 → 降低活跃槽优先级
|
||||
bool nextIsA = _activeSlot != 0;
|
||||
var inactiveCam = nextIsA ? _vcamA : _vcamB;
|
||||
var activeCam = nextIsA ? _vcamB : _vcamA;
|
||||
var inactiveConfiner = nextIsA ? _confinerA : _confinerB;
|
||||
if (fallBiasIdx >= 0 && fallBiasIdx > confinerIdx)
|
||||
Debug.LogError(
|
||||
$"[CameraStateController] VCam <b>{vcam.name}</b>:" +
|
||||
"CameraFallBiasExtension 必须在 CinemachineConfiner3D 之前!" +
|
||||
"当前顺序导致下坠偏置将相机推出限位区域而不被重新裁剪。" +
|
||||
"请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。");
|
||||
|
||||
ConfigureSlot(inactiveCam, inactiveConfiner, area);
|
||||
SyncFollowToVCam(inactiveCam); // 确保 Follow 正确(防止 SetFollowTarget 未被调用)
|
||||
inactiveCam.Priority = _globalActivePriority;
|
||||
activeCam.Priority = _standbyPriority; // 降到待机但仍 > 0,Brain 可在混合期间读取其状态
|
||||
_activeSlot = nextIsA ? 0 : 1;
|
||||
if (facingBiasIdx >= 0 && facingBiasIdx > confinerIdx)
|
||||
Debug.LogError(
|
||||
$"[CameraStateController] VCam <b>{vcam.name}</b>:" +
|
||||
"CameraFacingBiasExtension 必须在 CinemachineConfiner3D 之前!" +
|
||||
"当前顺序导致朝向偏置将相机推出限位区域而不被重新裁剪。" +
|
||||
"请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。");
|
||||
|
||||
if (adaptiveLahIdx >= 0 && adaptiveLahIdx > confinerIdx)
|
||||
Debug.LogError(
|
||||
$"[CameraStateController] VCam <b>{vcam.name}</b>:" +
|
||||
"CameraAdaptiveLookaheadExtension 必须在 CinemachineConfiner3D 之前!" +
|
||||
"请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。");
|
||||
|
||||
if (axisLockIdx >= 0 && axisLockIdx < confinerIdx)
|
||||
Debug.LogError(
|
||||
$"[CameraStateController] VCam <b>{vcam.name}</b>:" +
|
||||
"CameraAxisLockExtension 必须在 CinemachineConfiner3D 之后!" +
|
||||
"当前顺序导致轴向锁定被 Confiner 覆盖而失效。" +
|
||||
"请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -386,31 +415,18 @@ namespace BaseGames.Camera
|
||||
}
|
||||
|
||||
private static void ConfigureSlot(
|
||||
CinemachineCamera vcam, CinemachineConfiner2D confiner, CameraArea area)
|
||||
CinemachineCamera vcam, CinemachineConfiner3D confiner, CameraArea area)
|
||||
{
|
||||
// 1. Confiner
|
||||
if (confiner != null && area.ConfinerCollider != null)
|
||||
{
|
||||
confiner.BoundingShape2D = area.ConfinerCollider;
|
||||
// 限位多边形已在编辑器中预收缩(可视区域 - 视口半尺寸 = 相机中心运动范围)。
|
||||
// OversizeWindow.MaxWindowSize = 0.001f(极小正值):
|
||||
// 使 Cinemachine 将实际视口高度裁剪至 0.001,几乎不再对多边形额外收缩,
|
||||
// 从而以预收缩后的多边形直接作为相机中心约束边界。
|
||||
// 对于小于视口的房间(预收缩后多边形退化为点),仍正确固定相机于中心。
|
||||
confiner.OversizeWindow = new CinemachineConfiner2D.OversizeWindowSettings
|
||||
{
|
||||
Enabled = true,
|
||||
MaxWindowSize = 0.001f,
|
||||
Padding = 0.1f,
|
||||
};
|
||||
// BoundingShape2D 变更后必须刷新内部缓存路径,否则限位仍使用旧边界
|
||||
confiner.InvalidateLensCache();
|
||||
confiner.BoundingVolume = area.ConfinerCollider;
|
||||
}
|
||||
else if (confiner != null && area.ConfinerCollider == null)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[CameraStateController] {area.name} 未绑定 ConfinerCollider!" +
|
||||
"请将子节点 AreaBoundary 的 PolygonCollider2D 拖入 CameraArea._confinerCollider 字段。");
|
||||
"请将子节点 AreaBoundary 的 BoxCollider 拖入 CameraArea._confinerCollider 字段。");
|
||||
}
|
||||
|
||||
// 2. 跟随行为覆盖
|
||||
@@ -476,6 +492,26 @@ namespace BaseGames.Camera
|
||||
var fallBias = vcam.GetComponent<CameraFallBiasExtension>();
|
||||
if (fallBias != null)
|
||||
fallBias.SetConfiguredMax(area.DisableFallBias ? 0f : -1f);
|
||||
|
||||
// 5. 方向感知水平偏置
|
||||
// X 轴已锁定时强制关闭偏置:两者均在 Body Stage 执行,若偏置后于锁定运行会破坏轴锁。
|
||||
var facingBias = vcam.GetComponent<CameraFacingBiasExtension>();
|
||||
if (facingBias != null)
|
||||
{
|
||||
if (area.LockHorizontal)
|
||||
facingBias.FacingBias = 0f;
|
||||
else if (area.OverrideFacingBias)
|
||||
facingBias.FacingBias = area.FacingBiasOverride;
|
||||
}
|
||||
|
||||
// 6. 相机噪音(区域氛围震动:洞穴、水下、机械等)
|
||||
var noise = vcam.GetComponent<CinemachineBasicMultiChannelPerlin>();
|
||||
if (noise != null)
|
||||
{
|
||||
noise.NoiseProfile = area.NoiseProfile;
|
||||
noise.AmplitudeGain = area.NoiseProfile != null ? area.NoiseAmplitude : 0f;
|
||||
noise.FrequencyGain = area.NoiseFrequency;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResetVCamExtensions(CinemachineCamera vcam)
|
||||
@@ -484,6 +520,7 @@ namespace BaseGames.Camera
|
||||
vcam.GetComponent<CameraAsymmetricDampingExtension>()?.ResetState();
|
||||
vcam.GetComponent<CameraFallBiasExtension>()?.ResetState();
|
||||
vcam.GetComponent<CameraAdaptiveLookaheadExtension>()?.ResetState();
|
||||
vcam.GetComponent<CameraFacingBiasExtension>()?.ResetState();
|
||||
}
|
||||
|
||||
private void ApplyBlendProfile(CameraBlendProfileSO profile)
|
||||
@@ -546,15 +583,15 @@ namespace BaseGames.Camera
|
||||
|
||||
// 计算高度(先收集内容)
|
||||
string areaName = _currentArea != null ? _currentArea.name : "<无>";
|
||||
string slotLabel = _activeSlot < 0 ? "未初始化"
|
||||
: _activeSlot == 0 ? "VCam A"
|
||||
: "VCam B";
|
||||
string dedicatedLabel = _activeDedicatedCam != null
|
||||
? $"{_activeDedicatedCam.name} (P={_activeDedicatedCam.Priority})"
|
||||
: "<无激活 VCam>";
|
||||
string followLabel = _currentFollowTarget != null
|
||||
? _currentFollowTarget.name
|
||||
: "<未设置>";
|
||||
|
||||
bool warnFollow = _currentFollowTarget == null;
|
||||
bool warnNoVCam = _vcamA == null && _vcamB == null;
|
||||
bool warnNoVCam = _activeDedicatedCam == null;
|
||||
bool warnNoBrain = _brain == null;
|
||||
|
||||
// 区域状态(基线 + 触发区域集合)
|
||||
@@ -569,7 +606,7 @@ namespace BaseGames.Camera
|
||||
zoneLines.Add($" [{e.priority}] {(e.area != null ? e.area.name : "null")}{marker}");
|
||||
}
|
||||
|
||||
int lineCount = 5 + zoneLines.Count + (warnFollow ? 1 : 0) + (warnNoVCam ? 1 : 0) + (warnNoBrain ? 1 : 0);
|
||||
int lineCount = 4 + zoneLines.Count + (warnFollow ? 1 : 0) + (warnNoVCam ? 1 : 0) + (warnNoBrain ? 1 : 0);
|
||||
float rowH = 19f;
|
||||
float h = 28f + lineCount * rowH + 8f;
|
||||
|
||||
@@ -580,12 +617,7 @@ namespace BaseGames.Camera
|
||||
cy += 22f;
|
||||
|
||||
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), $"当前区域:{areaName}", _debugRowStyle); cy += rowH;
|
||||
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), $"活跃 VCam 槽:{slotLabel}", _debugRowStyle); cy += rowH;
|
||||
|
||||
string vcamALabel = _vcamA != null ? $"{_vcamA.name} (P={_vcamA.Priority})" : "<未绑定>";
|
||||
string vcamBLabel = _vcamB != null ? $"{_vcamB.name} (P={_vcamB.Priority})" : "<未绑定>";
|
||||
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), $"VCam A:{vcamALabel}", _debugRowStyle); cy += rowH;
|
||||
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), $"VCam B:{vcamBLabel}", _debugRowStyle); cy += rowH;
|
||||
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), $"专有 VCam:{dedicatedLabel}", warnNoVCam ? _debugWarnStyle : _debugRowStyle); cy += rowH;
|
||||
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), $"Follow 目标:{followLabel}", warnFollow ? _debugWarnStyle : _debugRowStyle); cy += rowH;
|
||||
|
||||
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), "区域状态(基线 + 触发区域):", _debugRowStyle); cy += rowH;
|
||||
@@ -598,7 +630,7 @@ namespace BaseGames.Camera
|
||||
if (warnFollow)
|
||||
{ GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), "⚠ Follow 目标未设置(检查 _onPlayerSpawned)", _debugWarnStyle); cy += rowH; }
|
||||
if (warnNoVCam)
|
||||
{ GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), "⚠ VCam A/B 均未绑定", _debugWarnStyle); cy += rowH; }
|
||||
{ GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), "⚠ 当前区域无激活专有 VCam(检查 DedicatedCamera 绑定)", _debugWarnStyle); cy += rowH; }
|
||||
if (warnNoBrain)
|
||||
{ GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), "⚠ CinemachineBrain 未绑定", _debugWarnStyle); cy += rowH; }
|
||||
}
|
||||
|
||||
@@ -5,10 +5,27 @@ using BaseGames.Core;
|
||||
|
||||
namespace BaseGames.Camera
|
||||
{
|
||||
/// <summary>
|
||||
/// 相机区域切换模式。
|
||||
/// </summary>
|
||||
public enum CameraZoneSwitchMode
|
||||
{
|
||||
/// <summary>进入即切换(默认)。
|
||||
/// 只要走进触发区域就立刻切换,不需要完全离开当前区域。</summary>
|
||||
Immediate,
|
||||
|
||||
/// <summary>必须离开当前区域才切换。
|
||||
/// 进入新区域后仅将其加入候选列表,等玩家完全离开当前激活区域后再接管。</summary>
|
||||
ExitFirst,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 相机区域切换触发器。
|
||||
/// 当触发区域重叠时,玩家必须先离开当前所在的触发区域,才会切换到下一个区域,
|
||||
/// 而不是进入重叠区域时立即切换。
|
||||
/// 支持两种切换模式,可通过 Inspector 配置:
|
||||
/// <list type="bullet">
|
||||
/// <item><b>Immediate</b>:进入即切换,不等待离开旧区域。</item>
|
||||
/// <item><b>ExitFirst</b>:必须离开当前激活区域后才切换。</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[ExecuteAlways]
|
||||
[RequireComponent(typeof(PolygonCollider2D))]
|
||||
@@ -24,17 +41,36 @@ namespace BaseGames.Camera
|
||||
"相同优先级则后进入的胜出(推荐默认值 1)。")]
|
||||
[SerializeField] private int _priority = 1;
|
||||
|
||||
[Tooltip("切换模式。\n" +
|
||||
"Immediate:进入即切换,无需离开当前区域(默认)。\n" +
|
||||
"ExitFirst:必须离开当前激活区域后才切换。")]
|
||||
[SerializeField] private CameraZoneSwitchMode _switchMode = CameraZoneSwitchMode.Immediate;
|
||||
|
||||
[SerializeField] private string _playerTag = "Player";
|
||||
|
||||
private PolygonCollider2D _collider;
|
||||
private bool _isPlayerInside;
|
||||
|
||||
/// <summary>触发区域优先级(只读),供外部按优先级选择最佳区域。</summary>
|
||||
public int Priority => _priority;
|
||||
|
||||
// ── 静态:跨实例共享触发状态 ──────────────────────────────────────────
|
||||
// 玩家当前物理上所在的所有触发区域(按进入顺序排列)
|
||||
private static readonly List<CameraTriggerZone> s_InsideZones = new();
|
||||
// 当前已向 ICameraService 发出 SwitchArea 请求的触发区域
|
||||
private static CameraTriggerZone s_ActiveZone;
|
||||
|
||||
/// <summary>
|
||||
/// 在每次进入 Play Mode 前(或禁用 Domain Reload 时的跨会话)重置静态状态,
|
||||
/// 防止上一次游戏会话残留的区域引用导致触发逻辑错误。
|
||||
/// </summary>
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void ResetStaticState()
|
||||
{
|
||||
s_InsideZones.Clear();
|
||||
s_ActiveZone = null;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_collider = GetComponent<PolygonCollider2D>();
|
||||
@@ -44,12 +80,7 @@ namespace BaseGames.Camera
|
||||
private void OnDisable()
|
||||
{
|
||||
if (!Application.isPlaying) return;
|
||||
if (!_isPlayerInside) return;
|
||||
|
||||
_isPlayerInside = false;
|
||||
s_InsideZones.Remove(this);
|
||||
if (s_ActiveZone == this)
|
||||
Deactivate(this);
|
||||
HandlePlayerExit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -76,46 +107,103 @@ namespace BaseGames.Camera
|
||||
s_InsideZones.Add(this);
|
||||
}
|
||||
|
||||
if (s_ActiveZone == null)
|
||||
Activate(this);
|
||||
EvaluateAndSwitch();
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (!Application.isPlaying) return;
|
||||
if (!other.CompareTag(_playerTag)) return;
|
||||
// 兼容碰撞体挂在子节点的玩家结构:先检查碰撞体本身标签,
|
||||
// 再检查其挂载的 Rigidbody2D 所在节点标签(通常为带标签的角色根节点)。
|
||||
if (!other.CompareTag(_playerTag) &&
|
||||
(other.attachedRigidbody == null || !other.attachedRigidbody.CompareTag(_playerTag)))
|
||||
return;
|
||||
if (_targetArea == null || _isPlayerInside) return;
|
||||
|
||||
_isPlayerInside = true;
|
||||
s_InsideZones.Add(this);
|
||||
|
||||
// 没有激活的触发区域 → 立即切换
|
||||
// 已有激活的触发区域 → 等玩家离开后再接管(避免重叠区域间提前切换)
|
||||
if (s_ActiveZone == null)
|
||||
Activate(this);
|
||||
// Immediate:进入即评估切换。
|
||||
// ExitFirst:仅在当前无激活区域时才先先激活,否则等待玩家离开当前激活区域。
|
||||
if (_switchMode == CameraZoneSwitchMode.Immediate || s_ActiveZone == null)
|
||||
EvaluateAndSwitch();
|
||||
}
|
||||
|
||||
private void OnTriggerExit2D(Collider2D other)
|
||||
{
|
||||
if (!Application.isPlaying) return;
|
||||
if (!other.CompareTag(_playerTag)) return;
|
||||
if (!_isPlayerInside) return;
|
||||
if (!other.CompareTag(_playerTag) &&
|
||||
(other.attachedRigidbody == null || !other.attachedRigidbody.CompareTag(_playerTag)))
|
||||
return;
|
||||
|
||||
// 复合碰撞体场景:某个子碰撞体退出时,验证玩家根节点是否仍在区域内。
|
||||
// 若根节点还在区域内(其他碰撞体尚未退出),则忽略此次退出事件。
|
||||
Transform playerRoot = other.attachedRigidbody != null
|
||||
? other.attachedRigidbody.transform
|
||||
: other.transform;
|
||||
if (_collider != null && _collider.OverlapPoint(playerRoot.position)) return;
|
||||
|
||||
HandlePlayerExit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 玩家离开触发区域的统一处理(<see cref="OnTriggerExit2D"/>、
|
||||
/// <see cref="FixedUpdate"/> 边缘检测及 <see cref="OnDisable"/> 共同调用)。
|
||||
/// 带幂等保护,多次调用安全。
|
||||
/// </summary>
|
||||
private void HandlePlayerExit()
|
||||
{
|
||||
if (!_isPlayerInside) return; // 幂等保护:防止重复触发
|
||||
|
||||
_isPlayerInside = false;
|
||||
s_InsideZones.Remove(this);
|
||||
|
||||
if (s_ActiveZone == this)
|
||||
Deactivate(this);
|
||||
else
|
||||
ServiceLocator.GetOrDefault<ICameraService>()?.ReleaseArea(_targetArea, null);
|
||||
}
|
||||
|
||||
// ── 静态辅助方法 ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 评估 <see cref="s_InsideZones"/> 并在需要时切换激活区域。
|
||||
/// <list type="bullet">
|
||||
/// <item>无激活区域时:直接激活最后进入的区域。</item>
|
||||
/// <item>新 <c>SelectBest()</c> 与当前激活不同时:立即覆盖切换。
|
||||
/// 由于 <c>SelectBest</c> 使用 >= 规则,进入任何新区域都会触发切换。</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
private static void EvaluateAndSwitch()
|
||||
{
|
||||
if (s_ActiveZone == null)
|
||||
{
|
||||
Activate(s_InsideZones[s_InsideZones.Count - 1]);
|
||||
return;
|
||||
}
|
||||
|
||||
CameraTriggerZone best = SelectBest();
|
||||
if (best != s_ActiveZone)
|
||||
OverrideActive(best);
|
||||
}
|
||||
|
||||
private static void Activate(CameraTriggerZone zone)
|
||||
{
|
||||
s_ActiveZone = zone;
|
||||
ServiceLocator.GetOrDefault<ICameraService>()?.SwitchArea(zone._targetArea, zone._priority);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 不经过 Exit 事件,直接将激活区域切换为 <paramref name="newZone"/>。
|
||||
/// 旧区域保留在 <see cref="s_InsideZones"/> 中(玩家仍在其内部),
|
||||
/// 不立即释放旧区域——等玩家物理离开旧区域时由 <see cref="OnTriggerExit2D"/> 清理。
|
||||
/// </summary>
|
||||
private static void OverrideActive(CameraTriggerZone newZone)
|
||||
{
|
||||
s_ActiveZone = newZone;
|
||||
ServiceLocator.GetOrDefault<ICameraService>()?.SwitchArea(newZone._targetArea, newZone._priority);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 离开 <paramref name="leaving"/> 时的处理:
|
||||
/// 若还有其他触发区域,先激活最优者再释放 leaving(避免短暂回退到房间基线);
|
||||
@@ -140,12 +228,16 @@ namespace BaseGames.Camera
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>从 <see cref="s_InsideZones"/> 中选出优先级最高的区域。</summary>
|
||||
/// <summary>
|
||||
/// 从 <see cref="s_InsideZones"/> 中选出最优区域。
|
||||
/// 优先级高者优先;优先级相同时取最后进入的区域(后进入的胜出),
|
||||
/// 确保进入任何新区域时都会立即切换,而不是等待离开旧区域。
|
||||
/// </summary>
|
||||
private static CameraTriggerZone SelectBest()
|
||||
{
|
||||
CameraTriggerZone best = s_InsideZones[0];
|
||||
for (int i = 1; i < s_InsideZones.Count; i++)
|
||||
if (s_InsideZones[i]._priority > best._priority)
|
||||
if (s_InsideZones[i]._priority >= best._priority) // >= 使同优先级时后进入的胜出
|
||||
best = s_InsideZones[i];
|
||||
return best;
|
||||
}
|
||||
@@ -155,19 +247,19 @@ namespace BaseGames.Camera
|
||||
if (_collider == null) _collider = GetComponent<PolygonCollider2D>();
|
||||
if (_collider == null || _collider.pathCount == 0) return;
|
||||
|
||||
var pts = new System.Collections.Generic.List<Vector2>();
|
||||
_collider.GetPath(0, pts);
|
||||
if (pts.Count < 2) return;
|
||||
|
||||
Gizmos.matrix = transform.localToWorldMatrix;
|
||||
Vector2 off = _collider.offset;
|
||||
|
||||
// 多边形触发边界(进入检测外框)
|
||||
Gizmos.color = new Color(0.2f, 0.8f, 1f, 0.8f);
|
||||
var pts = new List<Vector2>();
|
||||
_collider.GetPath(0, pts);
|
||||
for (int i = 0; i < pts.Count; i++)
|
||||
{
|
||||
Vector2 a = pts[i] + off;
|
||||
Vector2 b = pts[(i + 1) % pts.Count] + off;
|
||||
Gizmos.DrawLine(new Vector3(a.x, a.y), new Vector3(b.x, b.y));
|
||||
Vector3 a = new Vector3(pts[i].x, pts[i].y, 0f);
|
||||
Vector3 b = new Vector3(pts[(i + 1) % pts.Count].x, pts[(i + 1) % pts.Count].y, 0f);
|
||||
Gizmos.DrawLine(a, b);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using UnityEngine;
|
||||
using Unity.Cinemachine;
|
||||
|
||||
namespace BaseGames.Camera
|
||||
{
|
||||
@@ -27,7 +28,7 @@ namespace BaseGames.Camera
|
||||
/// </summary>
|
||||
void ReleaseArea(CameraArea releasedArea, CameraArea fallback);
|
||||
|
||||
/// <summary>为全局双 VCam 设置跟随目标(Player/CameraFollowTarget)。</summary>
|
||||
/// <summary>运行时设置跟随目标(Player/CameraFollowTarget),激活区域时自动同步到专属 VCam。</summary>
|
||||
void SetFollowTarget(Transform followTarget);
|
||||
|
||||
/// <summary>触发屏幕抖动(指定速度矢量)。</summary>
|
||||
@@ -37,9 +38,28 @@ namespace BaseGames.Camera
|
||||
void TriggerImpulse(float strength = 0.3f);
|
||||
|
||||
/// <summary>
|
||||
/// 平滑过渡正交相机尺寸。<paramref name="duration"/> = 0 时瞬间切换。
|
||||
/// 平滑过渡视野尺寸(可视半高,世界单位)。<paramref name="duration"/> = 0 时瞬间切换。
|
||||
/// 透视相机下自动换算为 FOV,与正交相机的 OrthographicSize 语义等价。
|
||||
/// 适用于 Boss 战拉远、特殊演出室拉近等场景。
|
||||
/// </summary>
|
||||
void SetLensSize(float orthographicSize, float duration = 0f);
|
||||
void SetLensSize(float visibleHalfHeight, float duration = 0f);
|
||||
|
||||
/// <summary>
|
||||
/// 进入过场模式,激活指定 VCam(优先级高于所有区域相机),Brain 自动混合。
|
||||
/// VCam 的 Follow / LookAt / Lens 由设计者在 Inspector 中配置,接口不强制覆写。
|
||||
/// </summary>
|
||||
void EnterCutsceneMode(CinemachineCamera cutsceneCamera);
|
||||
|
||||
/// <summary>
|
||||
/// 退出过场模式,撤销过场 VCam 的优先级,Brain 自动混合回当前区域相机。
|
||||
/// </summary>
|
||||
void ExitCutsceneMode();
|
||||
|
||||
/// <summary>
|
||||
/// 通知相机系统玩家的面朝方向,使方向感知偏置立即响应。
|
||||
/// 由 PlayerController 在精灵翻转时调用(<c>OnFlip</c>)。
|
||||
/// direction: +1 = 朝右,-1 = 朝左,0 = 清除外部输入(回退到速度估算)。
|
||||
/// </summary>
|
||||
void SetPlayerFacing(int direction);
|
||||
}
|
||||
}
|
||||
|
||||
44
Assets/_Game/Scripts/Camera/SpritePixelSnapper.cs
Normal file
44
Assets/_Game/Scripts/Camera/SpritePixelSnapper.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Camera
|
||||
{
|
||||
/// <summary>
|
||||
/// 精灵像素对齐组件。
|
||||
///
|
||||
/// 挂载在含 <see cref="SpriteRenderer"/> 的 GameObject 上(可与 Rigidbody2D 同节点)。
|
||||
/// 每渲染帧在 LateUpdate(+1000,与 <see cref="CameraPixelSnapper"/> 同执行序)中,
|
||||
/// 将 <c>transform.position</c> 四舍五入到最近像素网格,消除精灵移动时的亚像素模糊。
|
||||
///
|
||||
/// <para>
|
||||
/// 与 <see cref="CameraPixelSnapper"/> 搭配使用时,须保持 <c>_pixelsPerUnit</c>
|
||||
/// 与相机侧一致,使精灵和相机吸附到同一像素格,从而消除二者相对偏移造成的抖动。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Rigidbody2D 内部物理位置(<c>rb.position</c>)不受影响;
|
||||
/// 仅 <c>transform.position</c>(用于渲染)被修正,最大偏差 ±0.5 / PPU。
|
||||
/// 子节点(HurtBox、GroundCheck 等)随父节点微小平移,但偏差在物理容差范围内,
|
||||
/// 不影响碰撞检测精度。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(1000)]
|
||||
[AddComponentMenu("BaseGames/Camera/Sprite Pixel Snapper")]
|
||||
public class SpritePixelSnapper : MonoBehaviour
|
||||
{
|
||||
[Tooltip("每世界单位像素数(PPU)。须与 CameraPixelSnapper 及精灵导入设置保持一致。\n" +
|
||||
"常用值:16(粗像素风格)、32(标准像素风格)。0 = 禁用对齐。")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float _pixelsPerUnit = 16f;
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (_pixelsPerUnit <= 0f) return;
|
||||
|
||||
float ppu = _pixelsPerUnit;
|
||||
Vector3 pos = transform.position;
|
||||
pos.x = Mathf.Round(pos.x * ppu) / ppu;
|
||||
pos.y = Mathf.Round(pos.y * ppu) / ppu;
|
||||
// Z 轴不参与像素对齐
|
||||
transform.position = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Camera/SpritePixelSnapper.cs.meta
Normal file
11
Assets/_Game/Scripts/Camera/SpritePixelSnapper.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91f07a72af066da4684b9194b370a6f9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,3 +1,4 @@
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -10,8 +11,10 @@ namespace BaseGames.Combat
|
||||
/// 并携带 IgnoreIFrame 标记(无视翻滚/受击无敌帧,保证陷阱的绝对威胁性)。
|
||||
/// ② 若伤害导致玩家死亡 → PlayerController.TakeDamage 已 Raise EVT_PlayerDied,
|
||||
/// 走完整死亡流程(死亡动画 → 重载场景)。
|
||||
/// ③ 若玩家存活 → 本组件 Raise EVT_PlayerDied,强制返回最近检查点,
|
||||
/// 同样触发 DeathRespawnService(无死亡演出,只是重新加载)。
|
||||
/// ③ 若玩家存活且场景内有检查点 → Raise EVT_CheckpointRespawn,
|
||||
/// 由 CheckpointRespawnHandler 执行淡出→传送→淡入,不重载场景。
|
||||
/// ④ 若玩家存活但场景内无检查点 → Raise EVT_PlayerDied 作为兜底,
|
||||
/// 仍由 DeathRespawnService 重载场景回到存档点。
|
||||
///
|
||||
/// 可下劈(Pogo):
|
||||
/// - 将 _canPogo = true,并在地刺顶部添加子 GameObject:
|
||||
@@ -34,9 +37,12 @@ namespace BaseGames.Combat
|
||||
[SerializeField] private LayerMask _playerLayers;
|
||||
|
||||
[Header("事件")]
|
||||
[Tooltip("EVT_PlayerDied — 触发后由 DeathRespawnService 接管回到检查点")]
|
||||
[Tooltip("EVT_PlayerDied — 玩家存活但场景内无检查点时触发,走完整死亡/重载流程")]
|
||||
[SerializeField] private VoidEventChannelSO _onPlayerDied;
|
||||
|
||||
[Tooltip("EVT_CheckpointRespawn — 玩家存活且场景内有检查点时触发,执行原地传送")]
|
||||
[SerializeField] private VoidEventChannelSO _onCheckpointRespawn;
|
||||
|
||||
[Header("设计")]
|
||||
[Tooltip("true = 顶部有 HurtBox 子节点可下劈弹跳(需手动添加 Pogo Surface 子节点)")]
|
||||
[SerializeField] private bool _canPogo = true;
|
||||
@@ -85,10 +91,16 @@ namespace BaseGames.Combat
|
||||
}
|
||||
}
|
||||
|
||||
// ── 2. 若玩家存活(或无 HurtBox / damage=0),强制触发检查点回溯 ───────────
|
||||
// ── 2. 若玩家存活(或无 HurtBox / damage=0),回溯至最近检查点 ────────────
|
||||
if (!playerDiedFromDamage)
|
||||
{
|
||||
var cp = ServiceLocator.GetOrDefault<ICheckpointService>();
|
||||
if (cp != null && cp.HasCheckpoint)
|
||||
_onCheckpointRespawn?.Raise();
|
||||
else
|
||||
_onPlayerDied?.Raise();
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[UnityEngine.ContextMenu("打印 Pogo 提示")]
|
||||
|
||||
111
Assets/_Game/Scripts/Core/AutoSaveService.cs
Normal file
111
Assets/_Game/Scripts/Core/AutoSaveService.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 自动存档服务:在关键游戏事件发生后自动写盘,减少玩家因意外中断损失进度。
|
||||
///
|
||||
/// 触发时机:
|
||||
/// · 进入新房间 — EVT_SceneLoaded
|
||||
/// · 击败 Boss — EVT_BossFightEnded(bool=true 表示玩家获胜)
|
||||
/// · 获得新能力 — EVT_AbilityUnlockedStr
|
||||
/// · 购买物品 — EVT_ShopPurchase
|
||||
/// · 拾取关键物品 — EVT_CollectiblePickup
|
||||
/// · 拾取 HP 容器 — EVT_MaxHPContainerPickedUp
|
||||
/// · 开门/交互机关 — EVT_DoorOpened(使用钥匙、触发机关等)
|
||||
/// · 完成任务 — EVT_QuestStateChanged(State == Completed)
|
||||
///
|
||||
/// 自动存档使用与手动存档(存档点)相同的存档槽。
|
||||
/// 自动存档不会更改复活位置——SavePoint.OnSave 的 _isActivated 守卫确保
|
||||
/// 只有玩家已坐过的存档点才会写入 Player.Scene 和 Meta.SavePointId。
|
||||
///
|
||||
/// 防抖:同一冷却窗口内的多次触发合并为一次写盘操作,避免频繁 I/O。
|
||||
///
|
||||
/// 挂载位置:Persistent 场景根对象(与 GameServiceRegistrar 同级)。
|
||||
/// </summary>
|
||||
public class AutoSaveService : MonoBehaviour
|
||||
{
|
||||
[Header("触发事件频道")]
|
||||
[Tooltip("EVT_SceneLoaded — 进入新房间时自动存档")]
|
||||
[SerializeField] private StringEventChannelSO _onSceneLoaded;
|
||||
|
||||
[Tooltip("EVT_BossFightEnded — bool=true 表示玩家获胜,此时触发存档")]
|
||||
[SerializeField] private BoolEventChannelSO _onBossFightEnded;
|
||||
|
||||
[Tooltip("EVT_AbilityUnlockedStr — 获得新能力时触发存档")]
|
||||
[SerializeField] private StringEventChannelSO _onAbilityUnlocked;
|
||||
|
||||
[Tooltip("EVT_ShopPurchase — 购买物品后触发存档")]
|
||||
[SerializeField] private ShopPurchaseEventChannelSO _onShopPurchase;
|
||||
|
||||
[Tooltip("EVT_CollectiblePickup — 拾取关键物品(护符、道具等)后触发存档")]
|
||||
[SerializeField] private StringEventChannelSO _onCollectiblePickup;
|
||||
|
||||
[Tooltip("EVT_MaxHPContainerPickedUp — 拾取 HP 容器后触发存档")]
|
||||
[SerializeField] private StringEventChannelSO _onMaxHPContainerPickedUp;
|
||||
|
||||
[Tooltip("EVT_DoorOpened — 使用钥匙或触发机关开门后触发存档")]
|
||||
[SerializeField] private StringEventChannelSO _onDoorOpened;
|
||||
|
||||
[Tooltip("EVT_QuestStateChanged — 任务完成(State == Completed)时触发存档")]
|
||||
[SerializeField] private QuestStateChangedEventChannel _onQuestStateChanged;
|
||||
|
||||
[Header("防抖")]
|
||||
[Tooltip("两次自动存档之间的最短间隔(秒)。防止短时间内多次触发导致频繁写盘。")]
|
||||
[SerializeField] [Range(0.5f, 10f)] private float _cooldownSeconds = 2f;
|
||||
|
||||
private bool _onCooldown;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_onSceneLoaded? .Subscribe(OnSceneLoaded) .AddTo(_subs);
|
||||
_onBossFightEnded? .Subscribe(OnBossFightEnded) .AddTo(_subs);
|
||||
_onAbilityUnlocked? .Subscribe(_ => RequestAutoSave("ability")) .AddTo(_subs);
|
||||
_onShopPurchase? .Subscribe(_ => RequestAutoSave("shop")) .AddTo(_subs);
|
||||
_onCollectiblePickup? .Subscribe(_ => RequestAutoSave("collectible")) .AddTo(_subs);
|
||||
_onMaxHPContainerPickedUp? .Subscribe(_ => RequestAutoSave("hp_container")) .AddTo(_subs);
|
||||
_onDoorOpened? .Subscribe(_ => RequestAutoSave("door")) .AddTo(_subs);
|
||||
_onQuestStateChanged? .Subscribe(OnQuestStateChanged) .AddTo(_subs);
|
||||
}
|
||||
|
||||
private void OnDisable() => _subs.Clear();
|
||||
|
||||
// ── 触发处理 ───────────────────────────────────────────────────────────
|
||||
|
||||
private void OnSceneLoaded(string _) => RequestAutoSave("scene");
|
||||
private void OnBossFightEnded(bool won) { if (won) RequestAutoSave("boss"); }
|
||||
private void OnQuestStateChanged(QuestStateChangedEvent e)
|
||||
{
|
||||
if (e.State == QuestState.Completed) RequestAutoSave("quest_complete");
|
||||
}
|
||||
|
||||
// ── 防抖存档 ───────────────────────────────────────────────────────────
|
||||
|
||||
private void RequestAutoSave(string reason)
|
||||
{
|
||||
if (_onCooldown) return;
|
||||
StartCoroutine(DoAutoSave(reason));
|
||||
}
|
||||
|
||||
private IEnumerator DoAutoSave(string reason)
|
||||
{
|
||||
_onCooldown = true;
|
||||
|
||||
var svc = ServiceLocator.GetOrDefault<ISaveService>();
|
||||
if (svc != null)
|
||||
{
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
Debug.Log($"[AutoSave] 触发:{reason}");
|
||||
#endif
|
||||
// 与手动存档使用相同槽位,fire-and-forget
|
||||
_ = svc.SaveAsync(svc.ActiveSlot);
|
||||
}
|
||||
|
||||
yield return new WaitForSecondsRealtime(_cooldownSeconds);
|
||||
_onCooldown = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Core/AutoSaveService.cs.meta
Normal file
11
Assets/_Game/Scripts/Core/AutoSaveService.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9bdcd96d81e589642a250987222c25e2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
48
Assets/_Game/Scripts/Core/CheckpointService.cs
Normal file
48
Assets/_Game/Scripts/Core/CheckpointService.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 检查点服务实现。挂载在 Persistent 场景根对象上,由 GameServiceRegistrar 注册。
|
||||
/// 订阅 EVT_SceneLoaded,换房间时自动清空当前检查点。
|
||||
/// </summary>
|
||||
public class CheckpointService : MonoBehaviour, ICheckpointService
|
||||
{
|
||||
[Header("事件 - 监听")]
|
||||
[Tooltip("EVT_SceneLoaded — 收到后自动清空检查点")]
|
||||
[SerializeField] private StringEventChannelSO _onSceneLoaded;
|
||||
|
||||
private bool _hasCheckpoint;
|
||||
private Vector2 _checkpointPosition;
|
||||
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_onSceneLoaded?.Subscribe(OnSceneLoaded).AddTo(_subs);
|
||||
}
|
||||
|
||||
private void OnDisable() => _subs.Clear();
|
||||
|
||||
// ── ICheckpointService ────────────────────────────────────────────────
|
||||
|
||||
public bool HasCheckpoint => _hasCheckpoint;
|
||||
public Vector2 CheckpointPosition => _checkpointPosition;
|
||||
|
||||
public void RegisterCheckpoint(Vector2 position)
|
||||
{
|
||||
_hasCheckpoint = true;
|
||||
_checkpointPosition = position;
|
||||
}
|
||||
|
||||
public void ClearCheckpoint()
|
||||
{
|
||||
_hasCheckpoint = false;
|
||||
}
|
||||
|
||||
// ── Private ───────────────────────────────────────────────────────────
|
||||
|
||||
private void OnSceneLoaded(string _) => ClearCheckpoint();
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Core/CheckpointService.cs.meta
Normal file
11
Assets/_Game/Scripts/Core/CheckpointService.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 486e785d32d1c4c468a4eb0fd4cf1822
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -25,6 +25,7 @@ namespace BaseGames.Core
|
||||
[SerializeField] private SceneService _sceneService;
|
||||
[SerializeField] private EventChannelRegistry _eventChannelRegistry;
|
||||
[SerializeField] private GameSaveManager _saveManager;
|
||||
[SerializeField] private CheckpointService _checkpointService;
|
||||
/// <summary>
|
||||
/// Persistent 场景中唯一保留的主 AudioListener(通常挂在主相机上)。
|
||||
/// 在 Inspector 中绑定后可完全跳过 Awake 时的 FindObjectsOfType 全场景扫描。
|
||||
@@ -69,12 +70,18 @@ namespace BaseGames.Core
|
||||
else
|
||||
Debug.LogWarning("[GameServiceRegistrar] ⚠ _saveManager 未绑定,ISaveService 未注册。", this);
|
||||
|
||||
if (_checkpointService)
|
||||
ServiceLocator.Register<ICheckpointService>(_checkpointService);
|
||||
else
|
||||
Debug.LogWarning("[GameServiceRegistrar] ⚠ _checkpointService 未绑定,ICheckpointService 未注册。", this);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
var sb = new System.Text.StringBuilder("[GameServiceRegistrar] ✅ 服务注册完成:");
|
||||
if (_deathRespawnService) sb.Append(" IDeathRespawnService");
|
||||
if (_sceneService) sb.Append(" | ISceneService");
|
||||
if (_eventChannelRegistry) sb.Append(" | IEventChannelRegistry");
|
||||
if (_saveManager) sb.Append(" | ISaveService");
|
||||
if (_checkpointService) sb.Append(" | ICheckpointService");
|
||||
sb.Append(" | IAudioService(Null→等待覆盖)");
|
||||
Debug.Log(sb.ToString(), this);
|
||||
#endif
|
||||
|
||||
30
Assets/_Game/Scripts/Core/ICheckpointService.cs
Normal file
30
Assets/_Game/Scripts/Core/ICheckpointService.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 检查点服务接口。
|
||||
/// 运行时追踪玩家在当前房间内最近经过的检查点坐标,不持久化至存档。
|
||||
/// 换房间时由 CheckpointService 自动清空。
|
||||
/// </summary>
|
||||
public interface ICheckpointService
|
||||
{
|
||||
/// <summary>当前场景内是否存在已激活的检查点。</summary>
|
||||
bool HasCheckpoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 最近激活的检查点世界坐标。
|
||||
/// <see cref="HasCheckpoint"/> 为 false 时无意义。
|
||||
/// </summary>
|
||||
Vector2 CheckpointPosition { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 将指定位置登记为当前检查点(由 CheckpointMarker 调用)。
|
||||
/// 同一场景内多次调用时以最新值为准。
|
||||
/// </summary>
|
||||
void RegisterCheckpoint(Vector2 position);
|
||||
|
||||
/// <summary>清空当前检查点(换场景时自动调用)。</summary>
|
||||
void ClearCheckpoint();
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Core/ICheckpointService.cs.meta
Normal file
11
Assets/_Game/Scripts/Core/ICheckpointService.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 985eec4caa88c87428f183c8b6a6e6ae
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
243
Assets/_Game/Scripts/Editor/AbilityTypeDrawer.cs
Normal file
243
Assets/_Game/Scripts/Editor/AbilityTypeDrawer.cs
Normal file
@@ -0,0 +1,243 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using BaseGames.Player;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// AbilityType [Flags] uint 的 PropertyDrawer。
|
||||
/// 将枚举按能力类别分组,以可读的复选框网格呈现,替代默认的 MaskField。
|
||||
///
|
||||
/// 分组:
|
||||
/// 移动能力 — WallCling / WallJump / Dash / AirDash / DoubleJump / SuperJump / Swim / Dive
|
||||
/// 法术能力 — Spell1 / Spell2 / Spell3
|
||||
/// 形态能力 — SpiritForm / SpiritDash
|
||||
/// 战斗能力 — Parry / ChargeAttack / DownSlash
|
||||
/// 互动能力 — Interact / FastTravel
|
||||
/// 能力强化 — InvincibleDash
|
||||
/// </summary>
|
||||
[CustomPropertyDrawer(typeof(AbilityType))]
|
||||
public sealed class AbilityTypeDrawer : PropertyDrawer
|
||||
{
|
||||
// ── 分组定义 ──────────────────────────────────────────────────────────
|
||||
|
||||
private static readonly (string groupLabel, (AbilityType flag, string label)[] members)[] Groups =
|
||||
{
|
||||
("移动能力", new[]
|
||||
{
|
||||
(AbilityType.WallCling, "贴墙悬挂"),
|
||||
(AbilityType.WallJump, "墙跳"),
|
||||
(AbilityType.Dash, "冲刺"),
|
||||
(AbilityType.DoubleJump, "二段跳"),
|
||||
(AbilityType.SuperJump, "超级跳"),
|
||||
(AbilityType.Swim, "游泳"),
|
||||
(AbilityType.Dive, "下劈"),
|
||||
}),
|
||||
("法术能力", new[]
|
||||
{
|
||||
(AbilityType.Spell1, "法术槽 1"),
|
||||
(AbilityType.Spell2, "法术槽 2"),
|
||||
(AbilityType.Spell3, "法术槽 3"),
|
||||
}),
|
||||
("灵魄形态", new[]
|
||||
{
|
||||
(AbilityType.SpiritForm, "灵魄形态"),
|
||||
(AbilityType.SpiritDash, "灵魄冲刺"),
|
||||
}),
|
||||
("战斗能力", new[]
|
||||
{
|
||||
(AbilityType.Parry, "弹反"),
|
||||
(AbilityType.ChargeAttack, "蓄力攻击"),
|
||||
(AbilityType.DownSlash, "下斩"),
|
||||
}),
|
||||
("互动能力", new[]
|
||||
{
|
||||
(AbilityType.Interact, "互动"),
|
||||
(AbilityType.FastTravel, "快速旅行"),
|
||||
}),
|
||||
("能力强化", new[]
|
||||
{
|
||||
(AbilityType.InvincibleDash, "无敌冲刺"),
|
||||
}),
|
||||
};
|
||||
|
||||
// ── 布局常量 ──────────────────────────────────────────────────────────
|
||||
|
||||
private static readonly float RowH = EditorGUIUtility.singleLineHeight;
|
||||
private static readonly float GroupHeaderH = EditorGUIUtility.singleLineHeight + 4f;
|
||||
private static readonly float BtnRowH = EditorGUIUtility.singleLineHeight + 4f;
|
||||
private const float Spacing = 2f;
|
||||
private const float MinToggleW = 100f; // 每列最小宽度,用于动态计算列数
|
||||
private const int MaxColCount = 3; // 列数上限
|
||||
|
||||
// 缓存每个属性路径上次渲染时的列数,供 GetPropertyHeight 使用
|
||||
private static readonly Dictionary<string, int> _colsCache = new();
|
||||
|
||||
private static int ComputeCols(float availableWidth)
|
||||
=> Mathf.Clamp(Mathf.FloorToInt(availableWidth / MinToggleW), 1, MaxColCount);
|
||||
|
||||
// ── 高度计算 ──────────────────────────────────────────────────────────
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
float h = RowH + Spacing; // 属性标签行(含 None / All 按钮)
|
||||
|
||||
// 使用上次 OnGUI 缓存的列数;首次绘制前按视图宽度估算
|
||||
float viewW = EditorGUIUtility.currentViewWidth - EditorGUI.indentLevel * 15f - 18f;
|
||||
int cols = _colsCache.TryGetValue(property.propertyPath, out var cached)
|
||||
? cached
|
||||
: ComputeCols(viewW);
|
||||
|
||||
foreach (var (_, members) in Groups)
|
||||
{
|
||||
h += GroupHeaderH + Spacing;
|
||||
int rows = Mathf.CeilToInt((float)members.Length / cols);
|
||||
h += rows * (RowH + Spacing);
|
||||
}
|
||||
|
||||
return h + 4f;
|
||||
}
|
||||
|
||||
// ── 绘制 ──────────────────────────────────────────────────────────────
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
EditorGUI.BeginProperty(position, label, property);
|
||||
|
||||
float y = position.y;
|
||||
float x = position.x;
|
||||
float w = position.width;
|
||||
int cols = ComputeCols(w);
|
||||
_colsCache[property.propertyPath] = cols;
|
||||
|
||||
uint current = (uint)property.longValue;
|
||||
bool changed = false;
|
||||
|
||||
// ── 统计已启用数量 ────────────────────────────────────────────────
|
||||
int enabledCount = 0, totalCount = 0;
|
||||
foreach (var (_, mems) in Groups)
|
||||
foreach (var (flag, _) in mems)
|
||||
{
|
||||
totalCount++;
|
||||
if ((current & (uint)flag) != 0) enabledCount++;
|
||||
}
|
||||
|
||||
// ── 标签行 + None / All 快捷按钮 + 数量提示 ──────────────────────
|
||||
Rect labelRect = new Rect(x, y, EditorGUIUtility.labelWidth, RowH);
|
||||
Rect btnNoneRect = new Rect(x + EditorGUIUtility.labelWidth, y, 50f, RowH);
|
||||
Rect btnAllRect = new Rect(btnNoneRect.xMax + 4f, y, 50f, RowH);
|
||||
Rect countRect = new Rect(btnAllRect.xMax + 8f, y, w - (btnAllRect.xMax + 8f - x), RowH);
|
||||
|
||||
EditorGUI.LabelField(labelRect, label);
|
||||
|
||||
if (GUI.Button(btnNoneRect, "None"))
|
||||
{
|
||||
current = 0;
|
||||
changed = true;
|
||||
}
|
||||
if (GUI.Button(btnAllRect, "All"))
|
||||
{
|
||||
uint all = 0;
|
||||
foreach (var (_, mems) in Groups)
|
||||
foreach (var (flag, _) in mems)
|
||||
all |= (uint)flag;
|
||||
current = all;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
var countStyle = new GUIStyle(EditorStyles.miniLabel)
|
||||
{
|
||||
normal = { textColor = enabledCount > 0
|
||||
? new Color(0.55f, 0.85f, 0.55f)
|
||||
: new Color(0.6f, 0.6f, 0.6f) }
|
||||
};
|
||||
EditorGUI.LabelField(countRect, $"({enabledCount} / {totalCount} 项已解锁)", countStyle);
|
||||
y += RowH + Spacing;
|
||||
|
||||
// ── 各分组 ────────────────────────────────────────────────────────
|
||||
foreach (var (groupLabel, members) in Groups)
|
||||
{
|
||||
// 分组标题(含组级全选/清空按钮)
|
||||
Rect groupRect = new Rect(x, y, w, GroupHeaderH);
|
||||
uint newCurrent = DrawGroupHeader(groupRect, groupLabel, members, current);
|
||||
if (newCurrent != current) { current = newCurrent; changed = true; }
|
||||
y += GroupHeaderH + Spacing;
|
||||
|
||||
// 复选框网格(列宽均分可用宽度,列数随窗口大小自动调整)
|
||||
float toggleW = w / cols;
|
||||
for (int i = 0; i < members.Length; i++)
|
||||
{
|
||||
int col = i % cols;
|
||||
|
||||
if (col == 0 && i > 0)
|
||||
y += RowH + Spacing; // 换行
|
||||
|
||||
Rect togRect = new Rect(x + col * toggleW, y, toggleW, RowH);
|
||||
|
||||
var (flag, toggleLabel) = members[i];
|
||||
bool isOn = (current & (uint)flag) != 0;
|
||||
bool newOn = GUI.Toggle(togRect, isOn, toggleLabel, EditorStyles.toggle);
|
||||
|
||||
if (newOn != isOn)
|
||||
{
|
||||
if (newOn) current |= (uint)flag;
|
||||
else current &= ~(uint)flag;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// 若是最后一个且在本行
|
||||
if (i == members.Length - 1)
|
||||
y += RowH + Spacing;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
property.longValue = (long)(uint)current;
|
||||
property.serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
// ── 辅助:分组标题绘制(含组级全选/清空按钮与已选数量)────────────────
|
||||
|
||||
private static uint DrawGroupHeader(Rect rect, string text,
|
||||
(AbilityType flag, string label)[] members, uint current)
|
||||
{
|
||||
// 计算本组已选数量
|
||||
int groupEnabled = 0;
|
||||
foreach (var (flag, _) in members)
|
||||
if ((current & (uint)flag) != 0) groupEnabled++;
|
||||
|
||||
Color old = GUI.backgroundColor;
|
||||
GUI.backgroundColor = new Color(0.25f, 0.25f, 0.28f, 1f);
|
||||
GUI.Box(new Rect(rect.x, rect.y, rect.width, rect.height - 2f), GUIContent.none, EditorStyles.helpBox);
|
||||
GUI.backgroundColor = old;
|
||||
|
||||
// 分组标签(含已选/总数)
|
||||
Rect labelRect = new Rect(rect.x + 4f, rect.y + 1f, rect.width - 86f, RowH);
|
||||
EditorGUI.LabelField(labelRect,
|
||||
$"{text} {groupEnabled}/{members.Length}",
|
||||
EditorStyles.boldLabel);
|
||||
|
||||
// 组级按钮:全选 / 清空
|
||||
const float BtnW = 36f;
|
||||
Rect btnAll = new Rect(rect.xMax - BtnW * 2 - 6f, rect.y + 1f, BtnW, RowH);
|
||||
Rect btnNone = new Rect(rect.xMax - BtnW - 4f, rect.y + 1f, BtnW, RowH);
|
||||
|
||||
if (GUI.Button(btnAll, "全选", EditorStyles.miniButton))
|
||||
foreach (var (flag, _) in members) current |= (uint)flag;
|
||||
if (GUI.Button(btnNone, "清空", EditorStyles.miniButton))
|
||||
foreach (var (flag, _) in members) current &= ~(uint)flag;
|
||||
|
||||
// 底部分割线
|
||||
EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - 2f, rect.width, 1f),
|
||||
new Color(0.45f, 0.45f, 0.50f, 0.8f));
|
||||
|
||||
return current;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/AbilityTypeDrawer.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/AbilityTypeDrawer.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 60df999cbd27df94eb8ffd215c336b27
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -27,6 +27,7 @@
|
||||
"Kybernetik.Animancer",
|
||||
"BaseGames.Animation",
|
||||
"BaseGames.Equipment",
|
||||
"BaseGames.Parry",
|
||||
"BaseGames.Skills",
|
||||
"BaseGames.World.Map",
|
||||
"BaseGames.EventChain",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using Unity.Cinemachine;
|
||||
using BaseGames.Camera;
|
||||
@@ -17,7 +18,7 @@ namespace BaseGames.Editor
|
||||
/// FOV 优先级(降序):
|
||||
/// 专有 DedicatedCamera.Lens.FieldOfView
|
||||
/// → CameraLensConfigSO.fieldOfView(单一来源,无跨场景依赖)
|
||||
/// → CameraStateController._vcamA(Persistent 场景已加载时)
|
||||
/// → CameraStateController 活动 VCam 的 FieldOfView(编辑器备用)
|
||||
/// → Camera.main.fieldOfView
|
||||
/// → 60f(默认)
|
||||
/// </summary>
|
||||
@@ -41,6 +42,7 @@ namespace BaseGames.Editor
|
||||
private bool _foldFollow = true;
|
||||
private bool _foldLens = false;
|
||||
private bool _foldCamera = false;
|
||||
private bool _foldNoise = false;
|
||||
private bool _foldTools = false;
|
||||
|
||||
// ── 折叠标题样式缓存(深色背景 + 白色文字)────────────────────────────
|
||||
@@ -78,7 +80,7 @@ namespace BaseGames.Editor
|
||||
EditorGUILayout.PropertyField(confinerProp, new GUIContent("Confiner Collider"));
|
||||
}
|
||||
if (!confinerOk)
|
||||
EditorGUILayout.HelpBox("必须绑定子节点 PolygonCollider2D(AreaBoundary),否则 Cinemachine 无法限位。", MessageType.Error);
|
||||
EditorGUILayout.HelpBox("必须绑定子节点 BoxCollider(AreaBoundary),否则 CinemachineConfiner3D 无法限位。", MessageType.Error);
|
||||
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_visibleBounds"), new GUIContent("Visible Bounds(本地坐标)"));
|
||||
}
|
||||
@@ -86,11 +88,11 @@ namespace BaseGames.Editor
|
||||
|
||||
EditorGUILayout.Space(2f);
|
||||
|
||||
// ── 跟随参数覆盖 ─────────────────────────────────────────────────
|
||||
// ── 跟随行为参数 ─────────────────────────────────────────────────
|
||||
var overrideProp = serializedObject.FindProperty("_overrideFollowBehaviour");
|
||||
bool overrides = overrideProp.boolValue;
|
||||
_foldFollow = DrawFoldoutHeader(
|
||||
overrides ? "跟随参数覆盖 ●" : "跟随参数覆盖 ○ (使用全局默认)", _foldFollow);
|
||||
overrides ? "相机行为参数 ●" : "相机行为参数 ○ (使用 VCam 默认参数)", _foldFollow);
|
||||
if (_foldFollow)
|
||||
{
|
||||
using (new EditorGUI.IndentLevelScope())
|
||||
@@ -108,6 +110,25 @@ namespace BaseGames.Editor
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_lookaheadSmoothing"),new GUIContent("Lookahead Smoothing"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_lockHorizontal"), new GUIContent("Lock Horizontal"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_lockVertical"), new GUIContent("Lock Vertical"));
|
||||
|
||||
// ── 方向感知偏置 ──────────────────────────────────
|
||||
EditorGUILayout.Space(4f);
|
||||
EditorGUILayout.LabelField("方向感知水平偏置", EditorStyles.miniLabel);
|
||||
var overrideFacingProp = serializedObject.FindProperty("_overrideFacingBias");
|
||||
EditorGUILayout.PropertyField(overrideFacingProp, new GUIContent("Override Facing Bias",
|
||||
"开启后可为此区域单独设置偏置量;关闭则使用 VCam 扩展组件的默认值。"));
|
||||
if (overrideFacingProp.boolValue)
|
||||
{
|
||||
using (new EditorGUI.IndentLevelScope())
|
||||
{
|
||||
var biasProp = serializedObject.FindProperty("_facingBiasOverride");
|
||||
EditorGUILayout.PropertyField(biasProp, new GUIContent("Facing Bias (units)",
|
||||
"方向感知偏置量(世界单位)。0 = 禁用此区域的方向偏置。\n" +
|
||||
"较窄走廊建议设 0,防止相机超出 Confiner 边界。"));
|
||||
if (biasProp.floatValue < 0.01f)
|
||||
EditorGUILayout.HelpBox("设为 0 将禁用此区域的方向感知偏置。", MessageType.Info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,8 +149,8 @@ namespace BaseGames.Editor
|
||||
|
||||
EditorGUILayout.Space(2f);
|
||||
|
||||
// ── 专有相机(可选) ──────────────────────────────────────────────
|
||||
_foldCamera = DrawFoldoutHeader("专有相机(可选)", _foldCamera);
|
||||
// ── 虚拟相机 ──────────────────────────────────────────────
|
||||
_foldCamera = DrawFoldoutHeader("虚拟相机", _foldCamera);
|
||||
if (_foldCamera)
|
||||
{
|
||||
using (new EditorGUI.IndentLevelScope())
|
||||
@@ -140,7 +161,29 @@ namespace BaseGames.Editor
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(2f);
|
||||
// ── 相机噪音(氛围震动) ────────────────────────────────────────
|
||||
_foldNoise = DrawFoldoutHeader("相机噪音(氛围震动)", _foldNoise);
|
||||
if (_foldNoise)
|
||||
{
|
||||
using (new EditorGUI.IndentLevelScope())
|
||||
{
|
||||
var noiseProp = serializedObject.FindProperty("_noiseProfile");
|
||||
EditorGUILayout.PropertyField(noiseProp, new GUIContent("Noise Profile",
|
||||
"氛围震动配置(Noise Settings 资产),留空则禁用噪音。"));
|
||||
if (noiseProp.objectReferenceValue != null)
|
||||
{
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_noiseAmplitude"),
|
||||
new GUIContent("Amplitude Gain", "振幅增益(0 = 无震动,推荐 0.2 ~ 0.8)。"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_noiseFrequency"),
|
||||
new GUIContent("Frequency Gain", "频率增益(1 = 资产原始频率)。"));
|
||||
}
|
||||
EditorGUILayout.HelpBox(
|
||||
"专属 VCam 已包含 CinemachineBasicMultiChannelPerlin 组件(使用工具创建时自动附加)。",
|
||||
MessageType.Info);
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(2f);
|
||||
// ── 可视区域工具 ──────────────────────────────────────────────────
|
||||
_foldTools = DrawFoldoutHeader("可视区域工具", _foldTools);
|
||||
if (_foldTools)
|
||||
@@ -247,7 +290,7 @@ namespace BaseGames.Editor
|
||||
|
||||
EditorGUILayout.Space(4f);
|
||||
DrawLegend("■ 黄色矩形(Scene 视图)", kVisibleOutline, "可视区域 — 摄像机视口永不超出此范围");
|
||||
DrawLegend("■ 蓝色多边形(Scene 视图)", kConfinerLine, "限位区域 — CinemachineConfiner2D 的运动边界");
|
||||
DrawLegend("■ 蓝色矩形(Scene 视图)", kConfinerLine, "限位区域 — CinemachineConfiner3D 的运动边界");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +362,7 @@ namespace BaseGames.Editor
|
||||
{
|
||||
Undo.RecordObject(area, "Edit Visible Bounds");
|
||||
if (area.ConfinerCollider != null)
|
||||
Undo.RecordObject(area.ConfinerCollider, "Sync Confiner");
|
||||
Undo.RecordObject(area.ConfinerCollider, "Sync Confiner"); // BoxCollider
|
||||
|
||||
// 世界坐标 → 本地坐标,存入序列化字段
|
||||
boundsP.rectValue = new Rect(r.x - areaPos.x, r.y - areaPos.y, r.width, r.height);
|
||||
@@ -380,17 +423,21 @@ namespace BaseGames.Editor
|
||||
|
||||
private static void DrawConfinerGizmo(CameraArea area)
|
||||
{
|
||||
var poly = area.ConfinerCollider;
|
||||
if (poly == null || poly.pathCount == 0) return;
|
||||
var box = area.ConfinerCollider;
|
||||
if (box == null) return;
|
||||
|
||||
int ptCount = poly.GetTotalPointCount();
|
||||
if (ptCount < 3) return;
|
||||
// 将 BoxCollider 的 XY 范围投影到 Scene 视图(忽略 Z,2D 俯视)
|
||||
Vector3 centerWorld = box.transform.TransformPoint(box.center);
|
||||
float hw = box.size.x * 0.5f;
|
||||
float hh = box.size.y * 0.5f;
|
||||
|
||||
var pts2 = new System.Collections.Generic.List<Vector2>(ptCount);
|
||||
poly.GetPath(0, pts2);
|
||||
var pts3 = new Vector3[ptCount];
|
||||
for (int i = 0; i < ptCount; i++)
|
||||
pts3[i] = poly.transform.TransformPoint(pts2[i]);
|
||||
var pts3 = new Vector3[]
|
||||
{
|
||||
new Vector3(centerWorld.x - hw, centerWorld.y - hh, 0f), // BL
|
||||
new Vector3(centerWorld.x + hw, centerWorld.y - hh, 0f), // BR
|
||||
new Vector3(centerWorld.x + hw, centerWorld.y + hh, 0f), // TR
|
||||
new Vector3(centerWorld.x - hw, centerWorld.y + hh, 0f), // TL
|
||||
};
|
||||
|
||||
DrawPolyGizmo(pts3, kConfinerFill, kConfinerLine, 2.0f);
|
||||
|
||||
@@ -510,17 +557,16 @@ namespace BaseGames.Editor
|
||||
|
||||
internal static void SyncConfinerFromVisibleBounds(CameraArea area, float vFOV, float aspect)
|
||||
{
|
||||
var poly = area.ConfinerCollider;
|
||||
if (poly == null)
|
||||
var box = area.ConfinerCollider;
|
||||
if (box == null)
|
||||
{
|
||||
Debug.LogWarning($"[CameraAreaEditor] {area.name}:ConfinerCollider 未绑定,无法同步。");
|
||||
return;
|
||||
}
|
||||
|
||||
// VisibleBounds 已含 transform.position,为世界坐标。
|
||||
// 限位多边形 = 相机中心运动范围 = VisibleBounds 向内收缩视口半尺寸。
|
||||
// 运行时 ConfigureSlot 设置 OversizeWindow.MaxWindowSize ≈ 0,
|
||||
// 阻止 Cinemachine 再次收缩此多边形,确保边界精确匹配可视区域。
|
||||
// 限位体积 = 相机中心运动范围 = VisibleBounds 向内收缩视口半尺寸。
|
||||
// Confiner3D 以 BoxCollider 直接约束相机 3D 位置,无需 OversizeWindow 补偿。
|
||||
Rect visible = area.VisibleBounds; // 世界坐标
|
||||
float depth = area.CameraDepth;
|
||||
float halfH = depth * Mathf.Tan(vFOV * 0.5f * Mathf.Deg2Rad);
|
||||
@@ -532,25 +578,21 @@ namespace BaseGames.Editor
|
||||
float yMax = visible.yMax - halfH;
|
||||
|
||||
// 小房间:视口大于可视区域时收缩至中心点,相机固定在可视区域中心
|
||||
const float kMinSize = 0.001f;
|
||||
if (xMin > xMax) { float cx = visible.center.x; xMin = cx - kMinSize * 0.5f; xMax = cx + kMinSize * 0.5f; }
|
||||
if (yMin > yMax) { float cy = visible.center.y; yMin = cy - kMinSize * 0.5f; yMax = cy + kMinSize * 0.5f; }
|
||||
// BoxCollider 允许 size = 0,Confiner3D 不需要最小尺寸
|
||||
if (xMin > xMax) { float cx = visible.center.x; xMin = cx; xMax = cx; }
|
||||
if (yMin > yMax) { float cy = visible.center.y; yMin = cy; yMax = cy; }
|
||||
|
||||
Transform polyT = poly.transform;
|
||||
Vector2 Local(Vector3 w) => polyT.InverseTransformPoint(w);
|
||||
// BoxCollider center 以其 Transform 本地坐标表示
|
||||
// 相机在世界 Z = -depth,AreaBoundary 节点在 Z = 0,所以 center.z = -depth
|
||||
Transform boxT = box.transform;
|
||||
Vector3 centerWorld = new Vector3((xMin + xMax) * 0.5f, (yMin + yMax) * 0.5f, 0f);
|
||||
Vector3 centerLocal = boxT.InverseTransformPoint(centerWorld);
|
||||
centerLocal.z = -depth;
|
||||
|
||||
Undo.RecordObject(poly, "Sync Confiner from Visible Bounds");
|
||||
// 顶点必须 CCW(逆时针):Clipper 对 CW 多边形(area<0)会取反 delta,
|
||||
// 导致 Confiner 向外膨胀而非向内收缩,相机完全不受限。
|
||||
// CCW 顺序:BL → BR → TR → TL
|
||||
poly.SetPath(0, new[]
|
||||
{
|
||||
Local(new Vector3(xMin, yMin, 0f)), // BL
|
||||
Local(new Vector3(xMax, yMin, 0f)), // BR
|
||||
Local(new Vector3(xMax, yMax, 0f)), // TR
|
||||
Local(new Vector3(xMin, yMax, 0f)), // TL
|
||||
});
|
||||
EditorUtility.SetDirty(poly);
|
||||
Undo.RecordObject(box, "Sync Confiner from Visible Bounds");
|
||||
box.center = centerLocal;
|
||||
box.size = new Vector3(xMax - xMin, yMax - yMin, 1f);
|
||||
EditorUtility.SetDirty(box);
|
||||
|
||||
// 记录本次同步所用的 FOV,供编辑器过期检测使用
|
||||
var areaSO = new SerializedObject(area);
|
||||
@@ -575,6 +617,180 @@ namespace BaseGames.Editor
|
||||
internal static void SyncConfinerAuto(CameraArea area) =>
|
||||
SyncConfinerFromVisibleBounds(area, GetFOV(area), GetAspect());
|
||||
|
||||
// ── VCam 组件顺序检测 / 修复 ─────────────────────────────────────────
|
||||
|
||||
// 必须在 CinemachineConfiner3D 之前运行的扩展类型(修改 RawPosition,需被 Confiner 裁剪)
|
||||
private static readonly System.Type[] s_MustBeforeConfiner =
|
||||
{
|
||||
typeof(CameraAsymmetricDampingExtension),
|
||||
typeof(CameraFallBiasExtension),
|
||||
typeof(CameraFacingBiasExtension),
|
||||
typeof(CameraAdaptiveLookaheadExtension),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 检查 <paramref name="vcam"/> 的扩展组件顺序是否正确。
|
||||
/// 返回问题描述字符串;若无问题则返回 <c>null</c>。
|
||||
/// </summary>
|
||||
internal static string CheckVCamExtensionOrderIssue(CinemachineCamera vcam)
|
||||
{
|
||||
if (vcam == null) return null;
|
||||
|
||||
var confiner = vcam.GetComponent<CinemachineConfiner3D>();
|
||||
if (confiner == null) return "缺少 CinemachineConfiner3D 组件";
|
||||
|
||||
Component[] comps = vcam.GetComponents<Component>();
|
||||
int confinerIdx = System.Array.IndexOf(comps, confiner);
|
||||
var sb = new System.Text.StringBuilder();
|
||||
|
||||
foreach (var t in s_MustBeforeConfiner)
|
||||
{
|
||||
var comp = vcam.GetComponent(t);
|
||||
if (comp == null) continue;
|
||||
if (System.Array.IndexOf(comps, comp) > confinerIdx)
|
||||
sb.AppendLine($" · {t.Name} 在 Confiner 之后(偏置绕过限位 → 相机逃出区域)");
|
||||
}
|
||||
|
||||
var axisLock = vcam.GetComponent<CameraAxisLockExtension>();
|
||||
if (axisLock != null && System.Array.IndexOf(comps, axisLock) < confinerIdx)
|
||||
sb.AppendLine(" · CameraAxisLockExtension 在 Confiner 之前(轴向锁定会被 Confiner 覆盖失效)");
|
||||
|
||||
return sb.Length > 0 ? sb.ToString().TrimEnd() : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动修正 <paramref name="vcam"/> 上扩展组件的挂载顺序:
|
||||
/// 将偏置扩展移到 <see cref="CinemachineConfiner3D"/> 之前,
|
||||
/// 将 <see cref="CameraAxisLockExtension"/> 移到之后。
|
||||
/// </summary>
|
||||
internal static void FixVCamExtensionOrder(CinemachineCamera vcam)
|
||||
{
|
||||
if (vcam == null) return;
|
||||
|
||||
var confiner = vcam.GetComponent<CinemachineConfiner3D>();
|
||||
if (confiner == null)
|
||||
{
|
||||
Debug.LogWarning($"[CameraAreaEditor] {vcam.name} 缺少 CinemachineConfiner3D,无法修正顺序。");
|
||||
return;
|
||||
}
|
||||
|
||||
Undo.RegisterCompleteObjectUndo(vcam.gameObject, "Fix VCam Extension Order");
|
||||
|
||||
// 将偏置扩展逐个移到 Confiner 之前
|
||||
foreach (var t in s_MustBeforeConfiner)
|
||||
{
|
||||
var comp = vcam.GetComponent(t);
|
||||
if (comp == null) continue;
|
||||
|
||||
// 反复上移,直到位于 Confiner 之前(最多 30 步防死循环)
|
||||
for (int guard = 0; guard < 30; guard++)
|
||||
{
|
||||
Component[] comps = vcam.GetComponents<Component>();
|
||||
int compIdx = System.Array.IndexOf(comps, comp);
|
||||
int confinerIdx = System.Array.IndexOf(comps, confiner);
|
||||
if (compIdx < confinerIdx) break;
|
||||
UnityEditorInternal.ComponentUtility.MoveComponentUp(comp);
|
||||
}
|
||||
}
|
||||
|
||||
// 将 AxisLock 移到 Confiner 之后
|
||||
var axisLock = vcam.GetComponent<CameraAxisLockExtension>();
|
||||
if (axisLock != null)
|
||||
{
|
||||
for (int guard = 0; guard < 30; guard++)
|
||||
{
|
||||
Component[] comps = vcam.GetComponents<Component>();
|
||||
int axisIdx = System.Array.IndexOf(comps, axisLock);
|
||||
int confinerIdx = System.Array.IndexOf(comps, confiner);
|
||||
if (axisIdx > confinerIdx) break;
|
||||
UnityEditorInternal.ComponentUtility.MoveComponentDown(axisLock);
|
||||
}
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(vcam.gameObject);
|
||||
Debug.Log($"[CameraAreaEditor] 已修正 {vcam.name} 的扩展组件顺序。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为指定 <paramref name="area"/> 创建子节点专有 VCam,附加所有必要组件并绑定到
|
||||
/// <c>_dedicatedCamera</c>。若已有专有 VCam 则直接返回现有实例,不重复创建。
|
||||
/// </summary>
|
||||
internal static CinemachineCamera CreateDedicatedVCamForArea(CameraArea area)
|
||||
{
|
||||
if (area == null) return null;
|
||||
|
||||
if (area.DedicatedCamera != null)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[CameraAreaEditor] {area.name} 已有专有 VCam:{area.DedicatedCamera.name},跳过创建。");
|
||||
return area.DedicatedCamera;
|
||||
}
|
||||
|
||||
string vcamName = $"VCam_{area.gameObject.name}";
|
||||
var vcamGO = new GameObject(vcamName);
|
||||
Undo.RegisterCreatedObjectUndo(vcamGO, "Create Dedicated VCam");
|
||||
vcamGO.transform.SetParent(area.transform);
|
||||
vcamGO.transform.localPosition = Vector3.zero;
|
||||
|
||||
// ── CinemachineCamera ─────────────────────────────────────────────
|
||||
var vcam = vcamGO.AddComponent<CinemachineCamera>();
|
||||
vcam.Priority = 0; // 非激活;由 CameraStateController.ActivateDedicated 提升优先级
|
||||
|
||||
// ── Body:位置合成器 ───────────────────────────────────────────────
|
||||
var composer = vcamGO.AddComponent<CinemachinePositionComposer>();
|
||||
|
||||
// ── 扩展组件(管线顺序:Body 阶段偏置扩展 → PostBody Confiner → 独立 AxisLock → Noise)
|
||||
// AsymmetricDamping → FallBias → FacingBias → AdaptiveLookahead
|
||||
// vcamGO.AddComponent<CameraAsymmetricDampingExtension>();
|
||||
// vcamGO.AddComponent<CameraFallBiasExtension>();
|
||||
// vcamGO.AddComponent<CameraFacingBiasExtension>();
|
||||
// vcamGO.AddComponent<CameraAdaptiveLookaheadExtension>();
|
||||
vcamGO.AddComponent<CinemachinePixelPerfect>();
|
||||
// ── 限位 Confiner(PostBody 阶段,须在位置偏置扩展之后)────────────
|
||||
var confiner3d = vcamGO.AddComponent<CinemachineConfiner3D>();
|
||||
if (area.ConfinerCollider != null)
|
||||
confiner3d.BoundingVolume = area.ConfinerCollider;
|
||||
|
||||
// ── 轴向约束(独立,PostBody)────────────────────────────────────
|
||||
vcamGO.AddComponent<CameraAxisLockExtension>();
|
||||
|
||||
// ── 噪音(Noise 阶段,须排在 Confiner / AxisLock 之后)───────────
|
||||
vcamGO.AddComponent<CinemachineBasicMultiChannelPerlin>();
|
||||
|
||||
// ── 应用镜头参数:FOV + CameraDistance + Transform Z ─────────────
|
||||
// 与 CameraStateController.ApplyLensToVcam 逻辑保持一致:
|
||||
// 1. Lens.FieldOfView
|
||||
// 2. composer.CameraDistance(控制运行时真实 Z 距离,否则管线会覆盖)
|
||||
// 3. transform.localPosition.z(编辑器预览与运行时保持一致)
|
||||
float fov = area.LensConfig != null ? area.LensConfig.fieldOfView : 60f;
|
||||
float depth = area.CameraDepth > 0f ? area.CameraDepth : 10f;
|
||||
|
||||
var lens = vcam.Lens;
|
||||
lens.FieldOfView = fov;
|
||||
vcam.Lens = lens;
|
||||
|
||||
composer.CameraDistance = depth;
|
||||
|
||||
var localPos = vcamGO.transform.localPosition;
|
||||
localPos.z = -depth;
|
||||
vcamGO.transform.localPosition = localPos;
|
||||
|
||||
// ── 写入 CameraArea._dedicatedCamera(及默认优先级)───────────────
|
||||
var so = new SerializedObject(area);
|
||||
so.Update();
|
||||
so.FindProperty("_dedicatedCamera").objectReferenceValue = vcam;
|
||||
var priProp = so.FindProperty("_dedicatedPriority");
|
||||
if (priProp.intValue == 0) priProp.intValue = 20;
|
||||
so.ApplyModifiedProperties();
|
||||
EditorUtility.SetDirty(area);
|
||||
EditorSceneManager.MarkSceneDirty(area.gameObject.scene);
|
||||
|
||||
EditorGUIUtility.PingObject(vcamGO);
|
||||
Debug.Log($"[CameraAreaEditor] 已为 {area.name} 创建专有 VCam:{vcamName}" +
|
||||
$" FOV={fov:F1}° Depth={depth:F1}");
|
||||
return vcam;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 与 <see cref="SyncConfinerFromVisibleBounds"/> 逻辑相同,但不记录 Undo、不输出日志。
|
||||
/// 供拖拽 Handle 时每帧调用,避免 Undo 堆积和 Console 刷屏。
|
||||
@@ -582,8 +798,8 @@ namespace BaseGames.Editor
|
||||
/// </summary>
|
||||
private static void SyncConfinerQuiet(CameraArea area, float vFOV, float aspect)
|
||||
{
|
||||
var poly = area.ConfinerCollider;
|
||||
if (poly == null) return;
|
||||
var box = area.ConfinerCollider;
|
||||
if (box == null) return;
|
||||
|
||||
// VisibleBounds 已含 transform.position,为世界坐标。
|
||||
Rect visible = area.VisibleBounds;
|
||||
@@ -596,22 +812,17 @@ namespace BaseGames.Editor
|
||||
float yMin = visible.yMin + halfH;
|
||||
float yMax = visible.yMax - halfH;
|
||||
|
||||
const float kMinSize = 0.001f;
|
||||
if (xMin > xMax) { float cx = visible.center.x; xMin = cx - kMinSize * 0.5f; xMax = cx + kMinSize * 0.5f; }
|
||||
if (yMin > yMax) { float cy = visible.center.y; yMin = cy - kMinSize * 0.5f; yMax = cy + kMinSize * 0.5f; }
|
||||
if (xMin > xMax) { float cx = visible.center.x; xMin = cx; xMax = cx; }
|
||||
if (yMin > yMax) { float cy = visible.center.y; yMin = cy; yMax = cy; }
|
||||
|
||||
Transform polyT = poly.transform;
|
||||
Vector2 Local(Vector3 w) => polyT.InverseTransformPoint(w);
|
||||
Transform boxT = box.transform;
|
||||
Vector3 centerWorld = new Vector3((xMin + xMax) * 0.5f, (yMin + yMax) * 0.5f, 0f);
|
||||
Vector3 centerLocal = boxT.InverseTransformPoint(centerWorld);
|
||||
centerLocal.z = -depth;
|
||||
|
||||
// CCW 顺序:BL → BR → TR → TL(同 SyncConfinerFromVisibleBounds)
|
||||
poly.SetPath(0, new[]
|
||||
{
|
||||
Local(new Vector3(xMin, yMin, 0f)), // BL
|
||||
Local(new Vector3(xMax, yMin, 0f)), // BR
|
||||
Local(new Vector3(xMax, yMax, 0f)), // TR
|
||||
Local(new Vector3(xMin, yMax, 0f)), // TL
|
||||
});
|
||||
EditorUtility.SetDirty(poly);
|
||||
box.center = centerLocal;
|
||||
box.size = new Vector3(xMax - xMin, yMax - yMin, 1f);
|
||||
EditorUtility.SetDirty(box);
|
||||
}
|
||||
|
||||
/// <summary>在 Scene 视图左上角绘制叠加信息面板(屏幕空间)。</summary>
|
||||
@@ -663,7 +874,7 @@ namespace BaseGames.Editor
|
||||
// ══ 工具方法 ══════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于透视计算的 FOV(优先级:专有 VCam → 全局 VCamA → Camera.main → 60f)。
|
||||
/// 获取用于透视计算的 FOV(优先级:专属 VCam → CameraLensConfigSO → Camera.main → 60f)。
|
||||
/// </summary>
|
||||
private static float GetFOV(CameraArea area)
|
||||
{
|
||||
@@ -675,7 +886,7 @@ namespace BaseGames.Editor
|
||||
if (area.LensConfig != null)
|
||||
return area.LensConfig.fieldOfView;
|
||||
|
||||
// 3. Persistent 场景已加载时,实时读取全局 VCamA(兆底)
|
||||
// 3. CameraStateController 存在时,通过 LensConfig 读取 FOV(备用底线)
|
||||
#pragma warning disable UNT0023 // FindObjectOfType 在编辑器工具中可接受
|
||||
var ctrl = Object.FindObjectOfType<CameraStateController>();
|
||||
#pragma warning restore UNT0023
|
||||
|
||||
@@ -17,8 +17,9 @@ namespace BaseGames.Editor
|
||||
///
|
||||
/// 新格式:
|
||||
/// [新 CameraArea GO](CameraArea 组件,_visibleBounds = 本地 Rect)
|
||||
/// ├─ AreaBoundary(PolygonCollider2D,isTrigger=true,对应旧 Confiner)
|
||||
/// └─ TriggerZone(CameraTriggerZone + PolygonCollider2D,对应旧 TriggerRegion)
|
||||
/// ├─ AreaBoundary(BoxCollider,对应旧 Confiner)
|
||||
/// ├─ TriggerZone(CameraTriggerZone + PolygonCollider2D,对应旧 TriggerRegion)
|
||||
/// └─ VCam_xxx(CinemachineCamera + 所有扩展组件,专属虚拟相机)
|
||||
///
|
||||
/// 菜单:BaseGames → Camera → 相机区域迁移工具
|
||||
/// </summary>
|
||||
@@ -35,6 +36,7 @@ namespace BaseGames.Editor
|
||||
private Transform _sourcesParent; // 旧 Zone_xxx 的父节点(通常名为 Zones)
|
||||
private Transform _targetParent; // 新对象放置位置(留空 = 与旧区域同级)
|
||||
private CameraLensConfigSO _lensConfig; // 绑定到新 CameraArea._lensConfig
|
||||
private bool _createDedicatedVCam = true; // 为每个区域创建专属 CinemachineCamera
|
||||
|
||||
|
||||
// ── 运行时状态 ────────────────────────────────────────────────────────
|
||||
@@ -86,6 +88,12 @@ namespace BaseGames.Editor
|
||||
new GUIContent("镜头配置 SO", "赋给所有新 CameraArea._lensConfig;留空则不赋值"),
|
||||
_lensConfig, typeof(CameraLensConfigSO), false);
|
||||
|
||||
_createDedicatedVCam = EditorGUILayout.Toggle(
|
||||
new GUIContent("创建专属 VCam",
|
||||
"为每个迁移区域创建子节点 CinemachineCamera(含所有扩展组件),\n" +
|
||||
"并绑定到 CameraArea._dedicatedCamera。"),
|
||||
_createDedicatedVCam);
|
||||
|
||||
|
||||
|
||||
EditorGUILayout.Space(8);
|
||||
@@ -220,17 +228,24 @@ namespace BaseGames.Editor
|
||||
|
||||
foreach (Transform child in _sourcesParent)
|
||||
{
|
||||
Debug.Log($"[root]:{child.name}");
|
||||
// 旧 Zone 的标识:子节点直属,且挂有 BoxCollider2D
|
||||
var box = child.GetComponent<BoxCollider2D>();
|
||||
if (box == null) continue;
|
||||
|
||||
var entry = new ZoneEntry { ZoneObj = child.gameObject, VisibleBox = box };
|
||||
|
||||
// 收集触发多边形顶点(TriggerRegion 子节点的各个点对象)
|
||||
Transform triggerRoot = FindChildContaining(child, "TriggerRegion");
|
||||
if (triggerRoot != null)
|
||||
foreach (Transform pt in triggerRoot)
|
||||
// 收集触发多边形顶点——xxx_TriggerRegion 下每个子节点的世界坐标即一个顶点,
|
||||
// 按子节点顺序依次连线围成多边形触发区域。
|
||||
Transform triggerRoot = FindChildContaining(child, "Trigger");
|
||||
|
||||
if (triggerRoot != null){
|
||||
Debug.Log($"[trigger]:{triggerRoot.name}");
|
||||
foreach (Transform pt in triggerRoot){
|
||||
Debug.Log($"{pt.name}");
|
||||
entry.TriggerWorldPts.Add((Vector2)pt.position);
|
||||
}
|
||||
}
|
||||
|
||||
// 读取限位碰撞体(Zone_xxx_Confiner 上的 Collider2D)
|
||||
Transform confinerT = FindChildContaining(child, "Confiner");
|
||||
@@ -298,20 +313,20 @@ namespace BaseGames.Editor
|
||||
soArea.FindProperty("_lensConfig").objectReferenceValue = _lensConfig;
|
||||
soArea.ApplyModifiedProperties();
|
||||
|
||||
// ── 3. 创建 AreaBoundary(限位多边形,isTrigger = true)──────────
|
||||
// ── 3. 创建 AreaBoundary(限位体积 BoxCollider)─────────────────────────────
|
||||
GameObject boundaryGO = new GameObject($"{zoneGO.name}_AreaBoundary");
|
||||
Undo.RegisterCreatedObjectUndo(boundaryGO, "Migrate Camera Zone");
|
||||
boundaryGO.transform.SetParent(areaGO.transform, worldPositionStays: false);
|
||||
boundaryGO.transform.localPosition = Vector3.zero;
|
||||
|
||||
PolygonCollider2D confinerPoly = boundaryGO.AddComponent<PolygonCollider2D>();
|
||||
confinerPoly.isTrigger = true;
|
||||
confinerPoly.pathCount = 1;
|
||||
confinerPoly.SetPath(0, BuildConfinerPath(entry, worldPos, localBounds));
|
||||
BoxCollider confinerBox = boundaryGO.AddComponent<BoxCollider>();
|
||||
confinerBox.isTrigger = true;
|
||||
confinerBox.center = new Vector3(0f, 0f, -10f); // Z 占位符,SyncConfiner 会立即重算
|
||||
confinerBox.size = new Vector3(localBounds.width, localBounds.height, 1f);
|
||||
|
||||
// 绑定 _confinerCollider
|
||||
soArea.Update();
|
||||
soArea.FindProperty("_confinerCollider").objectReferenceValue = confinerPoly;
|
||||
soArea.FindProperty("_confinerCollider").objectReferenceValue = confinerBox;
|
||||
soArea.ApplyModifiedProperties();
|
||||
EditorUtility.SetDirty(area);
|
||||
|
||||
@@ -330,9 +345,10 @@ namespace BaseGames.Editor
|
||||
// AddComponent 会因 [RequireComponent] 自动添加 PolygonCollider2D
|
||||
CameraTriggerZone triggerComp = triggerGO.AddComponent<CameraTriggerZone>();
|
||||
PolygonCollider2D triggerPoly = triggerGO.GetComponent<PolygonCollider2D>();
|
||||
// 将旧触发多边形路径(本地坐标,相对于新 CameraArea 世界位置)直接赋给 PolygonCollider2D
|
||||
Vector2[] triggerPath = BuildTriggerPath(entry, worldPos, localBounds);
|
||||
triggerPoly.isTrigger = true;
|
||||
triggerPoly.pathCount = 1;
|
||||
triggerPoly.SetPath(0, BuildTriggerPath(entry, worldPos, localBounds));
|
||||
triggerPoly.SetPath(0, triggerPath);
|
||||
|
||||
// _targetArea → 指向刚创建的 CameraArea
|
||||
var soTrigger = new SerializedObject(triggerComp);
|
||||
@@ -340,7 +356,11 @@ namespace BaseGames.Editor
|
||||
soTrigger.ApplyModifiedProperties();
|
||||
EditorUtility.SetDirty(triggerComp);
|
||||
|
||||
// ── 5. 处理旧对象 ──────────────────────────────────────────────
|
||||
// ── 5. 创建专属 VCam(每区域独立相机)─────────────────────────────
|
||||
if (_createDedicatedVCam)
|
||||
CameraAreaEditor.CreateDedicatedVCamForArea(area);
|
||||
|
||||
// ── 6. 处理旧对象 ──────────────────────────────────────────────
|
||||
// 先记录原始激活状态,再对旧对象做处理,避免 SetActive(false) 后误读
|
||||
bool wasActive = zoneGO.activeSelf;
|
||||
|
||||
@@ -356,8 +376,8 @@ namespace BaseGames.Editor
|
||||
|
||||
Debug.Log($"[迁移工具] {zoneGO.name} → {areaGO.name} " +
|
||||
$"可视 {localBounds.width:F0}×{localBounds.height:F0} " +
|
||||
$"触发 {triggerPoly.GetTotalPointCount()} pt " +
|
||||
$"限位 {confinerPoly.GetTotalPointCount()} pt");
|
||||
$"触发 PolygonCollider2D ({triggerPath.Length} 点) " +
|
||||
$"限位 BoxCollider ({confinerBox.size.x:F1}×{confinerBox.size.y:F1})");
|
||||
}
|
||||
|
||||
// ── 限位多边形路径 ────────────────────────────────────────────────────
|
||||
@@ -405,14 +425,40 @@ namespace BaseGames.Editor
|
||||
{
|
||||
if (entry.TriggerWorldPts.Count >= 3)
|
||||
{
|
||||
var path = new Vector2[entry.TriggerWorldPts.Count];
|
||||
for (int i = 0; i < path.Length; i++)
|
||||
path[i] = entry.TriggerWorldPts[i] - (Vector2)areaWorldPos;
|
||||
return path;
|
||||
// 将世界坐标转换为 areaGO 本地坐标
|
||||
var localPts = new Vector2[entry.TriggerWorldPts.Count];
|
||||
for (int i = 0; i < localPts.Length; i++)
|
||||
localPts[i] = entry.TriggerWorldPts[i] - (Vector2)areaWorldPos;
|
||||
|
||||
// 按照质心角度排序,确保顶点顺序能够围成合法多边形
|
||||
return SortPointsByAngle(localPts);
|
||||
}
|
||||
return RectToPolygon(fallback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将一组点按照围绕质心的角度(逆时针)排序,使其能够围成合法的简单多边形。
|
||||
/// 适用于凸多边形及质心在多边形内部的凹多边形。
|
||||
/// </summary>
|
||||
private static Vector2[] SortPointsByAngle(Vector2[] points)
|
||||
{
|
||||
// 计算质心
|
||||
Vector2 centroid = Vector2.zero;
|
||||
foreach (var p in points)
|
||||
centroid += p;
|
||||
centroid /= points.Length;
|
||||
|
||||
// 按照相对质心的极角升序排列(逆时针)
|
||||
var sorted = new System.Collections.Generic.List<Vector2>(points);
|
||||
sorted.Sort((a, b) =>
|
||||
{
|
||||
float angleA = Mathf.Atan2(a.y - centroid.y, a.x - centroid.x);
|
||||
float angleB = Mathf.Atan2(b.y - centroid.y, b.x - centroid.x);
|
||||
return angleA.CompareTo(angleB);
|
||||
});
|
||||
return sorted.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动计算 BoxCollider2D 的世界 AABB,不依赖 .bounds(inactive 对象上 .bounds 无效)。
|
||||
/// </summary>
|
||||
|
||||
@@ -98,6 +98,9 @@ namespace BaseGames.Editor
|
||||
|
||||
if (GUILayout.Button("↺ 全部同步限位区域", EditorStyles.toolbarButton))
|
||||
SyncAllConfiners();
|
||||
|
||||
if (GUILayout.Button("✔ 批量创建专属 VCam", EditorStyles.toolbarButton))
|
||||
BatchCreateDedicatedVCams();
|
||||
}
|
||||
|
||||
// ── 创建 CameraArea 面板 ───────────────────────────────────────
|
||||
@@ -217,6 +220,40 @@ namespace BaseGames.Editor
|
||||
|
||||
Debug.Log($"[CameraAreaSetupTool] 已同步 {count} 个 CameraArea 的限位区域。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为所有尚未配置专有 VCam 的 CameraArea 批量创建专有 CinemachineCamera。
|
||||
/// 已有 _dedicatedCamera 的区域将跳过。
|
||||
/// </summary>
|
||||
private void BatchCreateDedicatedVCams()
|
||||
{
|
||||
if (_cameraAreas.Count == 0)
|
||||
{
|
||||
Debug.LogWarning("[CameraAreaSetupTool] 场景中无 CameraArea,跳过批量创建。");
|
||||
return;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
foreach (var area in _cameraAreas)
|
||||
{
|
||||
if (area == null) continue;
|
||||
if (area.DedicatedCamera != null) continue; // 已有专有 VCam,跳过
|
||||
|
||||
CameraAreaEditor.CreateDedicatedVCamForArea(area);
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
RescanScene();
|
||||
Debug.Log($"[CameraAreaSetupTool] 已为 {count} 个 CameraArea 创建专有 VCam。");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("[CameraAreaSetupTool] 所有 CameraArea 均已有专有 VCam,无需创建。");
|
||||
}
|
||||
}
|
||||
|
||||
// ── CameraStateController ──────────────────────────────────────────
|
||||
|
||||
private void DrawControllerSection()
|
||||
@@ -240,8 +277,6 @@ namespace BaseGames.Editor
|
||||
}
|
||||
|
||||
SerializedObject so = new SerializedObject(_controller);
|
||||
DrawFieldCheck(so, "_vcamA", "全局 VCam A (CinemachineCamera)");
|
||||
DrawFieldCheck(so, "_vcamB", "全局 VCam B (CinemachineCamera)");
|
||||
DrawFieldCheck(so, "_brain", "CinemachineBrain");
|
||||
DrawFieldCheck(so, "_onPlayerSpawned", "玩家生成事件 (EVT_PlayerSpawned) → VCam 自动绑 Follow");
|
||||
DrawFieldCheck(so, "_impulseSource", "CinemachineImpulseSource", optional: true);
|
||||
@@ -334,7 +369,8 @@ namespace BaseGames.Editor
|
||||
bool confinerOk = so.FindProperty("_confinerCollider").objectReferenceValue != null;
|
||||
var boundZones = FindTriggerZonesForArea(area);
|
||||
bool hasZone = boundZones.Count > 0;
|
||||
bool allOk = confinerOk && hasZone;
|
||||
bool hasVCam = so.FindProperty("_dedicatedCamera").objectReferenceValue != null;
|
||||
bool allOk = confinerOk && hasZone && hasVCam;
|
||||
|
||||
using (new EditorGUILayout.VerticalScope(_boxStyle))
|
||||
{
|
||||
@@ -353,6 +389,10 @@ namespace BaseGames.Editor
|
||||
GUI.color = hasZone ? kOk : kError;
|
||||
GUILayout.Label(hasZone ? $"[{boundZones.Count} 触发器]" : "[无触发器]",
|
||||
EditorStyles.miniLabel, GUILayout.Width(74f));
|
||||
|
||||
GUI.color = hasVCam ? kOk : kError;
|
||||
GUILayout.Label(hasVCam ? "[VCam ✔]" : "[VCam ✗]",
|
||||
EditorStyles.miniLabel, GUILayout.Width(54f));
|
||||
GUI.color = prevC;
|
||||
|
||||
if (GUILayout.Button("⊙", GUILayout.Width(24f)))
|
||||
@@ -366,12 +406,61 @@ namespace BaseGames.Editor
|
||||
EditorGUILayout.Space(3f);
|
||||
|
||||
// ── 绑定字段 ────────────────────────────────────────────────
|
||||
DrawCheckRow("_confinerCollider(可视边界 PolygonCollider2D)", confinerOk);
|
||||
DrawCheckRow("_dedicatedCamera(专有 VCam,可选)",
|
||||
so.FindProperty("_dedicatedCamera").objectReferenceValue != null, optional: true);
|
||||
DrawCheckRow("_confinerCollider(可视边界 BoxCollider)", confinerOk);
|
||||
|
||||
// ── 专有 VCam 状态行(创建 / Ping 按鈕)────────────────────────
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
Color prevC2 = GUI.color;
|
||||
GUI.color = hasVCam ? kOk : kError;
|
||||
GUILayout.Label(hasVCam ? "●" : "✗", GUILayout.Width(16f));
|
||||
GUI.color = prevC2;
|
||||
|
||||
if (hasVCam)
|
||||
{
|
||||
var vcamObj = so.FindProperty("_dedicatedCamera").objectReferenceValue;
|
||||
EditorGUILayout.LabelField($"_dedicatedCamera:{vcamObj.name}",
|
||||
GUILayout.ExpandWidth(true));
|
||||
if (GUILayout.Button("⊙", GUILayout.Width(24f)))
|
||||
EditorGUIUtility.PingObject(vcamObj);
|
||||
if (GUILayout.Button("选中", GUILayout.Width(36f)))
|
||||
Selection.activeObject = vcamObj;
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.LabelField("_dedicatedCamera(专有 VCam)未创建",
|
||||
GUILayout.ExpandWidth(true));
|
||||
if (GUILayout.Button("创建专有 VCam", GUILayout.Width(90f), GUILayout.Height(18f)))
|
||||
{
|
||||
CameraAreaEditor.CreateDedicatedVCamForArea(area);
|
||||
RescanScene();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DrawCheckRow("_blendProfile(可选,未设则用全局默认)",
|
||||
so.FindProperty("_blendProfile").objectReferenceValue != null, optional: true);
|
||||
|
||||
// ── VCam 扩展组件顺序检查 ────────────────────────────────────
|
||||
// AsymmetricDamping/FallBias/FacingBias 必须在 CinemachineConfiner3D 之前;
|
||||
// AxisLock 必须在之后。顺序错误会使相机逃出限位区域。
|
||||
if (hasVCam)
|
||||
{
|
||||
var vcam = so.FindProperty("_dedicatedCamera").objectReferenceValue as CinemachineCamera;
|
||||
string issue = CameraAreaEditor.CheckVCamExtensionOrderIssue(vcam);
|
||||
if (issue != null)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
$"VCam 扩展组件顺序错误!相机会逃出限位区域:\n{issue}",
|
||||
MessageType.Error);
|
||||
if (GUILayout.Button("⚙ 自动修正组件顺序", GUILayout.Height(22f)))
|
||||
{
|
||||
CameraAreaEditor.FixVCamExtensionOrder(vcam);
|
||||
RescanScene();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 触发区域列表 ─────────────────────────────────────────────
|
||||
EditorGUILayout.Space(3f);
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
@@ -412,16 +501,16 @@ namespace BaseGames.Editor
|
||||
EditorGUILayout.Space(3f);
|
||||
if (!confinerOk)
|
||||
{
|
||||
// 区分:有非 Trigger 的 PolygonCollider2D 可直接绑定 vs 完全没有 AreaBoundary
|
||||
var existingBoundary = FindBoundaryPoly(area);
|
||||
// 区分:有 BoxCollider 可直接绑定 vs 完全没有 AreaBoundary
|
||||
var existingBoundary = FindBoundaryBox(area);
|
||||
if (existingBoundary != null)
|
||||
{
|
||||
if (GUILayout.Button("修复:绑定子节点 PolygonCollider2D", GUILayout.Height(22f)))
|
||||
if (GUILayout.Button("修复:绑定子节点 BoxCollider", GUILayout.Height(22f)))
|
||||
FixConfinerBinding(area);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GUILayout.Button("创建 AreaBoundary(限位多边形,默认 24 × 12)", GUILayout.Height(22f)))
|
||||
if (GUILayout.Button("创建 AreaBoundary(限位体积,默认 24 × 12)", GUILayout.Height(22f)))
|
||||
{
|
||||
CreateAreaBoundary(area);
|
||||
RescanScene();
|
||||
@@ -433,7 +522,7 @@ namespace BaseGames.Editor
|
||||
Color helpC = GUI.color;
|
||||
GUI.color = kMuted;
|
||||
EditorGUILayout.LabelField(
|
||||
"★ 可视边界:选中子节点的 PolygonCollider2D,在 Scene 视图中编辑顶点。",
|
||||
"★ 限位体积:选中子节点的 BoxCollider,在 Inspector 中编辑 Center / Size。",
|
||||
EditorStyles.miniLabel);
|
||||
GUI.color = helpC;
|
||||
}
|
||||
@@ -557,34 +646,34 @@ namespace BaseGames.Editor
|
||||
Debug.LogWarning("[CameraAreaSetupTool] _vcamA/_vcamB 均未绑定,无法赋值 Follow。请先在 Inspector 中绑定。");
|
||||
}
|
||||
|
||||
/// <summary>将子节点中找到的第一个不含 CameraTriggerZone 的 PolygonCollider2D 绑定到 CameraArea._confinerCollider。</summary>
|
||||
/// <summary>将子节点中找到的第一个不含 CameraTriggerZone 的 BoxCollider 绑定到 CameraArea._confinerCollider。</summary>
|
||||
private static void FixConfinerBinding(CameraArea area)
|
||||
{
|
||||
PolygonCollider2D poly = FindBoundaryPoly(area)
|
||||
?? area.GetComponentInChildren<PolygonCollider2D>(true);
|
||||
if (poly == null)
|
||||
BoxCollider box = FindBoundaryBox(area)
|
||||
?? area.GetComponentInChildren<BoxCollider>(true);
|
||||
if (box == null)
|
||||
{
|
||||
Debug.LogWarning($"[CameraAreaSetupTool] {area.name}:子节点中未找到 PolygonCollider2D。");
|
||||
Debug.LogWarning($"[CameraAreaSetupTool] {area.name}:子节点中未找到 BoxCollider。");
|
||||
return;
|
||||
}
|
||||
|
||||
SerializedObject so = new SerializedObject(area);
|
||||
so.FindProperty("_confinerCollider").objectReferenceValue = poly;
|
||||
so.FindProperty("_confinerCollider").objectReferenceValue = box;
|
||||
so.ApplyModifiedProperties();
|
||||
|
||||
Debug.Log($"[CameraAreaSetupTool] {area.name}:_confinerCollider → {poly.gameObject.name}");
|
||||
Debug.Log($"[CameraAreaSetupTool] {area.name}:_confinerCollider → {box.gameObject.name}");
|
||||
}
|
||||
|
||||
/// <summary>返回 area 子节点中第一个不含 CameraTriggerZone 的 PolygonCollider2D(即 AreaBoundary 限位体)。</summary>
|
||||
private static PolygonCollider2D FindBoundaryPoly(CameraArea area)
|
||||
/// <summary>返回 area 子节点中第一个不含 CameraTriggerZone 的 BoxCollider(即 AreaBoundary 限位体)。</summary>
|
||||
private static BoxCollider FindBoundaryBox(CameraArea area)
|
||||
{
|
||||
foreach (var p in area.GetComponentsInChildren<PolygonCollider2D>(true))
|
||||
if (p.GetComponent<CameraTriggerZone>() == null) return p;
|
||||
foreach (var b in area.GetComponentsInChildren<BoxCollider>(true))
|
||||
if (b.GetComponent<CameraTriggerZone>() == null) return b;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为指定 CameraArea 创建 AreaBoundary 子节点(默认矩形限位多边形,isTrigger = false)并绑定到 _confinerCollider。
|
||||
/// 为指定 CameraArea 创建 AreaBoundary 子节点(默认 BoxCollider 限位体)并绑定到 _confinerCollider。
|
||||
/// </summary>
|
||||
private static void CreateAreaBoundary(CameraArea area)
|
||||
{
|
||||
@@ -602,27 +691,20 @@ namespace BaseGames.Editor
|
||||
childGo.transform.localPosition = Vector3.zero;
|
||||
}
|
||||
|
||||
PolygonCollider2D poly = childGo.GetComponent<PolygonCollider2D>()
|
||||
?? childGo.AddComponent<PolygonCollider2D>();
|
||||
poly.isTrigger = true; // 限位多边形,仅作为相机约束边界,不产生物理碰撞
|
||||
poly.pathCount = 1;
|
||||
poly.SetPath(0, new Vector2[]
|
||||
{
|
||||
new Vector2(-12f, -6f),
|
||||
new Vector2(-12f, 6f),
|
||||
new Vector2( 12f, 6f),
|
||||
new Vector2( 12f, -6f),
|
||||
});
|
||||
BoxCollider box = childGo.GetComponent<BoxCollider>()
|
||||
?? childGo.AddComponent<BoxCollider>();
|
||||
box.center = new Vector3(0f, 0f, -10f); // Z 占位符,绑定 LensConfig 后点击「同步限位区域」更新
|
||||
box.size = new Vector3(24f, 12f, 1f); // 默认 24 × 12 占位符
|
||||
EditorUtility.SetDirty(childGo);
|
||||
|
||||
SerializedObject so = new SerializedObject(area);
|
||||
so.Update();
|
||||
so.FindProperty("_confinerCollider").objectReferenceValue = poly;
|
||||
so.FindProperty("_confinerCollider").objectReferenceValue = box;
|
||||
so.ApplyModifiedProperties();
|
||||
EditorUtility.SetDirty(area);
|
||||
|
||||
EditorGUIUtility.PingObject(childGo);
|
||||
Debug.Log($"[CameraAreaSetupTool] 已为 {area.name} 创建 AreaBoundary(矩形 24 × 12)。");
|
||||
Debug.Log($"[CameraAreaSetupTool] 已为 {area.name} 创建 AreaBoundary(BoxCollider 默认 24 × 12)。");
|
||||
}
|
||||
|
||||
/// <summary>返回所有以此 area 为激活目标的 CameraTriggerZone。</summary>
|
||||
@@ -642,18 +724,10 @@ namespace BaseGames.Editor
|
||||
/// <summary>为指定 CameraArea 创建配对的 CameraTriggerZone,自动匹配 Confiner 范围。</summary>
|
||||
private static void CreateTriggerZoneForArea(CameraArea area)
|
||||
{
|
||||
// 用 PolygonCollider2D 包围盒作为放置中心和尺寸;没有则退回到 area 自身位置
|
||||
Vector3 center = area.transform.position;
|
||||
Vector2 size = new Vector2(4f, 4f);
|
||||
|
||||
var poly = area.GetComponentInChildren<PolygonCollider2D>(true);
|
||||
if (poly != null)
|
||||
{
|
||||
Bounds b = poly.bounds;
|
||||
center = b.center;
|
||||
center.z = area.transform.position.z;
|
||||
size = new Vector2(b.size.x, b.size.y);
|
||||
}
|
||||
// 用 VisibleBounds 作为放置中心和初始多边形范围
|
||||
Rect visible = area.VisibleBounds;
|
||||
Vector3 center = new Vector3(visible.center.x, visible.center.y, area.transform.position.z);
|
||||
Vector2 half = visible.size * 0.5f;
|
||||
|
||||
var go = new GameObject($"{area.gameObject.name}_TriggerZone");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Create CameraTriggerZone");
|
||||
@@ -661,19 +735,20 @@ namespace BaseGames.Editor
|
||||
go.transform.SetParent(area.transform);
|
||||
go.transform.position = center;
|
||||
|
||||
var col = go.AddComponent<PolygonCollider2D>();
|
||||
// [RequireComponent] 会自动附加 PolygonCollider2D;先 AddComponent<CameraTriggerZone>
|
||||
// 再通过 GetComponent 引用,避免顺序依赖问题
|
||||
var zone = go.AddComponent<CameraTriggerZone>();
|
||||
var col = go.GetComponent<PolygonCollider2D>();
|
||||
col.isTrigger = true;
|
||||
float hw = size.x * 0.5f;
|
||||
float hh = size.y * 0.5f;
|
||||
// 以 VisibleBounds 矩形四角为默认路径(可在 Inspector 中进一步编辑顶点)
|
||||
col.SetPath(0, new Vector2[]
|
||||
{
|
||||
new Vector2(-hw, -hh),
|
||||
new Vector2(-hw, hh),
|
||||
new Vector2( hw, hh),
|
||||
new Vector2( hw, -hh),
|
||||
new Vector2(-half.x, -half.y),
|
||||
new Vector2(-half.x, half.y),
|
||||
new Vector2( half.x, half.y),
|
||||
new Vector2( half.x, -half.y),
|
||||
});
|
||||
|
||||
var zone = go.AddComponent<CameraTriggerZone>();
|
||||
var so = new SerializedObject(zone);
|
||||
so.Update();
|
||||
so.FindProperty("_targetArea").objectReferenceValue = area;
|
||||
|
||||
835
Assets/_Game/Scripts/Editor/CharacterWizardWindow.cs
Normal file
835
Assets/_Game/Scripts/Editor/CharacterWizardWindow.cs
Normal file
@@ -0,0 +1,835 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using BaseGames.Boss;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Enemies;
|
||||
using BaseGames.Equipment;
|
||||
using BaseGames.Input;
|
||||
using BaseGames.Parry;
|
||||
using BaseGames.Player;
|
||||
using BaseGames.Player.States;
|
||||
using BaseGames.Skills;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色创建向导(W-01)— 统一入口窗口。
|
||||
/// 技术:UI Toolkit,三标签页(玩家 / 小怪 / Boss)。
|
||||
/// 菜单:BaseGames / Tools / Character Wizard
|
||||
///
|
||||
/// 各标签页均提供:
|
||||
/// ① 当前 SO 资产状态速览(绿色=已存在 / 橙色=缺失)
|
||||
/// ② SO 资产工厂(一键创建所需的所有 ScriptableObject)
|
||||
/// ③ 场景搭建快捷按钮(调用 SceneObjectPlacerTool.PlaceXxx)
|
||||
/// ④ 跳转到对应专项编辑器窗口
|
||||
/// </summary>
|
||||
public class CharacterWizardWindow : EditorWindow
|
||||
{
|
||||
// ── 常量 ──────────────────────────────────────────────────────────────
|
||||
private const string UssPath = "Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss";
|
||||
private const string DataRoot = "Assets/_Game/Data";
|
||||
|
||||
private static StyleSheet _uss;
|
||||
private static StyleSheet Uss =>
|
||||
_uss != null ? _uss : (_uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath));
|
||||
|
||||
[MenuItem("BaseGames/Tools/Character Wizard", priority = 1)]
|
||||
public static void Open()
|
||||
{
|
||||
var wnd = GetWindow<CharacterWizardWindow>();
|
||||
wnd.titleContent = new GUIContent("Character Wizard", EditorGUIUtility.IconContent("d_Prefab Icon").image);
|
||||
wnd.minSize = new Vector2(520, 600);
|
||||
}
|
||||
|
||||
// ── 状态 ──────────────────────────────────────────────────────────────
|
||||
private int _activeTab = 0;
|
||||
private Button _btnPlayer, _btnEnemy, _btnBoss;
|
||||
|
||||
// SO 状态缓存(避免每帧重查)
|
||||
private List<(string label, bool exists)> _playerSOStatus = new();
|
||||
private List<(string label, bool exists)> _enemySOStatus = new();
|
||||
private List<(string label, bool exists)> _bossSOStatus = new();
|
||||
private double _lastRefreshTime;
|
||||
|
||||
// 小怪类型选择
|
||||
private int _enemyTypeIndex = 0;
|
||||
private static readonly string[] EnemyTypeLabels = { "普通(近战)", "远程", "飞行" };
|
||||
|
||||
// Boss 命名字段
|
||||
private string _bossId = "NewBoss";
|
||||
private string _enemyId = "NewEnemy";
|
||||
private string _playerId = "Player";
|
||||
|
||||
// SO 状态面板(按标签页缓存)
|
||||
private VisualElement _playerStatusPanel;
|
||||
private VisualElement _enemyStatusPanel;
|
||||
private VisualElement _bossStatusPanel;
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
|
||||
public void CreateGUI()
|
||||
{
|
||||
if (Uss != null)
|
||||
rootVisualElement.styleSheets.Add(Uss);
|
||||
|
||||
rootVisualElement.style.flexDirection = FlexDirection.Column;
|
||||
|
||||
BuildTabBar();
|
||||
BuildTabContents();
|
||||
RefreshSOStatus();
|
||||
SwitchTab(0);
|
||||
}
|
||||
|
||||
private void OnFocus() => RefreshSOStatus();
|
||||
|
||||
// ── 标签栏 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void BuildTabBar()
|
||||
{
|
||||
var bar = new VisualElement();
|
||||
bar.AddToClassList("tab-bar");
|
||||
|
||||
_btnPlayer = MakeTabButton("玩家", () => SwitchTab(0));
|
||||
_btnEnemy = MakeTabButton("小怪", () => SwitchTab(1));
|
||||
_btnBoss = MakeTabButton("Boss", () => SwitchTab(2));
|
||||
|
||||
bar.Add(_btnPlayer);
|
||||
bar.Add(_btnEnemy);
|
||||
bar.Add(_btnBoss);
|
||||
rootVisualElement.Add(bar);
|
||||
}
|
||||
|
||||
private Button MakeTabButton(string label, Action onClick)
|
||||
{
|
||||
var btn = new Button(onClick) { text = label };
|
||||
btn.AddToClassList("tab-btn");
|
||||
return btn;
|
||||
}
|
||||
|
||||
private void SwitchTab(int idx)
|
||||
{
|
||||
_activeTab = idx;
|
||||
var tabs = rootVisualElement.Query<VisualElement>(className: "tab-content").ToList();
|
||||
for (int i = 0; i < tabs.Count; i++)
|
||||
tabs[i].style.display = (i == idx) ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
|
||||
_btnPlayer.EnableInClassList("tab-btn--active", idx == 0);
|
||||
_btnEnemy .EnableInClassList("tab-btn--active", idx == 1);
|
||||
_btnBoss .EnableInClassList("tab-btn--active", idx == 2);
|
||||
}
|
||||
|
||||
// ── 标签页内容 ────────────────────────────────────────────────────────
|
||||
|
||||
private void BuildTabContents()
|
||||
{
|
||||
rootVisualElement.Add(BuildPlayerTab());
|
||||
rootVisualElement.Add(BuildEnemyTab());
|
||||
rootVisualElement.Add(BuildBossTab());
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// 玩家标签页
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
private VisualElement BuildPlayerTab()
|
||||
{
|
||||
var root = MakeTabContent();
|
||||
|
||||
root.Add(MakeSectionHeader("▶ SO 资产状态"));
|
||||
_playerStatusPanel = new VisualElement();
|
||||
root.Add(_playerStatusPanel);
|
||||
|
||||
root.Add(MakeSectionHeader("▶ SO 资产工厂"));
|
||||
root.Add(MakeHelpBox("在 Project 中创建下列 ScriptableObject 资产。若已存在则跳过。"));
|
||||
|
||||
var idRow = MakeLabeledTextField("资产名称前缀", _playerId, v => _playerId = v);
|
||||
root.Add(idRow);
|
||||
|
||||
var factory = MakeActionGroup();
|
||||
factory.Add(MakeFactoryButton("PlayerStatsSO", () => CreatePlayerStat()));
|
||||
factory.Add(MakeFactoryButton("PlayerMovementConfigSO", () => CreateMovementConfig()));
|
||||
factory.Add(MakeFactoryButton("PlayerAnimationConfigSO", () => CreateAnimConfig()));
|
||||
factory.Add(MakeFactoryButton("FormConfigSO + 3 FormSO", () => CreateFormConfig()));
|
||||
factory.Add(MakeFactoryButton("WeaponSO × 3(形态)", () => CreateFormWeapons()));
|
||||
factory.Add(MakeFactoryButton("DamageSourceSO(连击×3)", () => CreatePlayerDamageSources()));
|
||||
factory.Add(MakeFactoryButton("ParryConfigSO", () => CreateParryConfig()));
|
||||
factory.Add(MakeFactoryButton("ShieldConfigSO", () => CreateShieldConfig()));
|
||||
factory.Add(MakeFactoryButton("EquipmentConfigSO", () => CreateEquipmentConfig()));
|
||||
factory.Add(MakeFactoryButton("CharmCatalogSO", () => CreateCharmCatalog()));
|
||||
root.Add(factory);
|
||||
|
||||
var createAllBtn = new Button(CreateAllPlayerSOs) { text = "★ 一键创建全部 Player SO" };
|
||||
createAllBtn.style.marginTop = 6;
|
||||
createAllBtn.style.height = 26;
|
||||
root.Add(createAllBtn);
|
||||
|
||||
root.Add(MakeSeparator());
|
||||
root.Add(MakeSectionHeader("▶ 场景搭建"));
|
||||
root.Add(MakeHelpBox("在当前活动场景中放置玩家 GameObject(带完整组件树)。"));
|
||||
|
||||
var sceneGroup = MakeActionGroup();
|
||||
sceneGroup.Add(MakeSceneButton("放置玩家到场景", SceneObjectPlacerTool.PlacePlayer));
|
||||
sceneGroup.Add(MakeSceneButton("指定所有 SO 到场景角色", AssignAllPlayerSOsToScene));
|
||||
sceneGroup.Add(MakeSceneButton("放置地面平台", SceneObjectPlacerTool.PlaceGroundPlatform));
|
||||
sceneGroup.Add(MakeSceneButton("放置存档点", SceneObjectPlacerTool.PlaceSavePoint));
|
||||
root.Add(sceneGroup);
|
||||
|
||||
root.Add(MakeSeparator());
|
||||
root.Add(MakeSectionHeader("▶ 专项编辑器"));
|
||||
|
||||
var jumpGroup = MakeActionGroup();
|
||||
jumpGroup.Add(MakeJumpButton("武器编辑器", () => Combat.WeaponEditorWindow.Open()));
|
||||
jumpGroup.Add(MakeJumpButton("技能编辑器", () => Skills.SkillEditorWindow.Open()));
|
||||
jumpGroup.Add(MakeJumpButton("形态编辑器", () => FormEditorWindow.Open()));
|
||||
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
||||
root.Add(jumpGroup);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// 小怪标签页
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
private VisualElement BuildEnemyTab()
|
||||
{
|
||||
var root = MakeTabContent();
|
||||
|
||||
root.Add(MakeSectionHeader("▶ SO 资产状态"));
|
||||
_enemySOStatus.Clear();
|
||||
_enemyStatusPanel = new VisualElement();
|
||||
root.Add(_enemyStatusPanel);
|
||||
|
||||
root.Add(MakeSectionHeader("▶ 敌人类型选择"));
|
||||
|
||||
var typeRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginBottom = 4 } };
|
||||
for (int i = 0; i < EnemyTypeLabels.Length; i++)
|
||||
{
|
||||
int captured = i;
|
||||
var btn = new Button(() =>
|
||||
{
|
||||
_enemyTypeIndex = captured;
|
||||
// 高亮激活按钮(简单刷新所有同类按钮样式)
|
||||
RefreshEnemyTypeButtons(root);
|
||||
})
|
||||
{ text = EnemyTypeLabels[i] };
|
||||
btn.name = $"enemy-type-{i}";
|
||||
btn.EnableInClassList("type-btn--active", i == _enemyTypeIndex);
|
||||
typeRow.Add(btn);
|
||||
}
|
||||
root.Add(typeRow);
|
||||
|
||||
root.Add(MakeSectionHeader("▶ SO 资产工厂"));
|
||||
root.Add(MakeHelpBox("每个敌人建议独立命名,便于 Loot / BD 资产管理。"));
|
||||
|
||||
var idRow = MakeLabeledTextField("敌人 ID", _enemyId, v => _enemyId = v);
|
||||
root.Add(idRow);
|
||||
|
||||
var factory = MakeActionGroup();
|
||||
factory.Add(MakeFactoryButton("EnemyStatsSO", () => CreateEnemyStat()));
|
||||
factory.Add(MakeFactoryButton("LootTableSO", () => CreateLootTable()));
|
||||
factory.Add(MakeFactoryButton("AttackPatternSO × 2", () => CreateEnemyAttackPatterns()));
|
||||
factory.Add(MakeFactoryButton("DamageSourceSO", () => CreateEnemyDamageSource()));
|
||||
root.Add(factory);
|
||||
|
||||
root.Add(MakeSeparator());
|
||||
root.Add(MakeSectionHeader("▶ 场景搭建"));
|
||||
root.Add(MakeHelpBox("根据选中的类型在场景中生成对应的敌人 GameObject。"));
|
||||
|
||||
var sceneGroup = MakeActionGroup();
|
||||
sceneGroup.Add(MakeSceneButton("放置敌人到场景", PlaceSelectedEnemyType));
|
||||
root.Add(sceneGroup);
|
||||
|
||||
root.Add(MakeSeparator());
|
||||
root.Add(MakeSectionHeader("▶ 专项编辑器"));
|
||||
|
||||
var jumpGroup = MakeActionGroup();
|
||||
jumpGroup.Add(MakeJumpButton("敌人数据管理", () => Enemies.EnemyDataWindow.Open()));
|
||||
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
||||
root.Add(jumpGroup);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// Boss 标签页
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
private VisualElement BuildBossTab()
|
||||
{
|
||||
var root = MakeTabContent();
|
||||
|
||||
root.Add(MakeSectionHeader("▶ SO 资产状态"));
|
||||
_bossStatusPanel = new VisualElement();
|
||||
root.Add(_bossStatusPanel);
|
||||
|
||||
root.Add(MakeSectionHeader("▶ SO 资产工厂"));
|
||||
root.Add(MakeHelpBox("每个 Boss 独立目录:Assets/_Game/Data/Boss/<BossId>/"));
|
||||
|
||||
var idRow = MakeLabeledTextField("Boss ID", _bossId, v => _bossId = v);
|
||||
root.Add(idRow);
|
||||
|
||||
var factory = MakeActionGroup();
|
||||
factory.Add(MakeFactoryButton("EnemyStatsSO(Boss)", () => CreateBossStat()));
|
||||
factory.Add(MakeFactoryButton("LootTableSO(Boss)", () => CreateBossLoot()));
|
||||
factory.Add(MakeFactoryButton("AttackPatternSO × 3(阶段)", () => CreateBossAttackPatterns()));
|
||||
factory.Add(MakeFactoryButton("BossSkillSO × 3", () => CreateBossSkills()));
|
||||
factory.Add(MakeFactoryButton("SkillSequenceSO(Phase 1)", () => CreateBossSkillSequence(1)));
|
||||
factory.Add(MakeFactoryButton("SkillSequenceSO(Phase 2)", () => CreateBossSkillSequence(2)));
|
||||
factory.Add(MakeFactoryButton("DamageSourceSO × 3", () => CreateBossDamageSources()));
|
||||
root.Add(factory);
|
||||
|
||||
root.Add(MakeSeparator());
|
||||
root.Add(MakeSectionHeader("▶ 场景搭建"));
|
||||
|
||||
var sceneGroup = MakeActionGroup();
|
||||
sceneGroup.Add(MakeSceneButton("放置 Boss 到场景", SceneObjectPlacerTool.PlaceBossEnemy));
|
||||
root.Add(sceneGroup);
|
||||
|
||||
root.Add(MakeSeparator());
|
||||
root.Add(MakeSectionHeader("▶ 专项编辑器"));
|
||||
|
||||
var jumpGroup = MakeActionGroup();
|
||||
jumpGroup.Add(MakeJumpButton("Boss 技能序列查看器", BossSkillSequenceWindow.OpenWindow));
|
||||
jumpGroup.Add(MakeJumpButton("敌人数据管理", () => Enemies.EnemyDataWindow.Open()));
|
||||
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
||||
root.Add(jumpGroup);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
// ── SO 资产工厂:玩家 ────────────────────────────────────────────────
|
||||
|
||||
private void CreatePlayerStat()
|
||||
{
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<PlayerStatsSO>(
|
||||
$"{DataRoot}/Player", "PLY_PlayerStats");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateMovementConfig()
|
||||
{
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<PlayerMovementConfigSO>(
|
||||
$"{DataRoot}/Player", "PLY_PlayerMovementConfig");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateAnimConfig()
|
||||
{
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<PlayerAnimationConfigSO>(
|
||||
$"{DataRoot}/Player", "PLY_PlayerAnimationConfig");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateFormConfig()
|
||||
{
|
||||
string configDir = $"{DataRoot}/Player";
|
||||
string formsDir = $"{DataRoot}/Player/Forms";
|
||||
EditorScaffoldUtils.EnsureFolder(formsDir);
|
||||
|
||||
var cfg = EditorScaffoldUtils.CreateSOAsset<FormConfigSO>(configDir, "PLY_FormConfig");
|
||||
if (cfg == null) cfg = AssetDatabase.LoadAssetAtPath<FormConfigSO>($"{configDir}/PLY_FormConfig.asset");
|
||||
|
||||
var formTypes = new[] { ("TianHun", FormType.TianHun, "天魂"), ("DiHun", FormType.DiHun, "地魂"), ("MingHun", FormType.MingHun, "命魂") };
|
||||
var forms = new List<FormSO>();
|
||||
foreach (var (id, ftype, dname) in formTypes)
|
||||
{
|
||||
string path = $"{formsDir}/PLY_Form_{id}.asset";
|
||||
var form = AssetDatabase.LoadAssetAtPath<FormSO>(path);
|
||||
if (form == null)
|
||||
{
|
||||
form = ScriptableObject.CreateInstance<FormSO>();
|
||||
form.formId = $"Form_{id}";
|
||||
form.displayName = dname;
|
||||
form.formType = ftype;
|
||||
AssetDatabase.CreateAsset(form, path);
|
||||
}
|
||||
forms.Add(form);
|
||||
}
|
||||
|
||||
if (cfg != null && (cfg.forms == null || cfg.forms.Length == 0))
|
||||
{
|
||||
cfg.forms = forms.ToArray();
|
||||
EditorUtility.SetDirty(cfg);
|
||||
}
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
EditorGUIUtility.PingObject(cfg);
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateFormWeapons()
|
||||
{
|
||||
string dir = $"{DataRoot}/Combat/Weapons";
|
||||
foreach (var id in new[] { "TianHun", "DiHun", "MingHun" })
|
||||
EditorScaffoldUtils.CreateSOAsset<WeaponSO>(dir, $"WPN_{id}");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreatePlayerDamageSources()
|
||||
{
|
||||
string dir = $"{DataRoot}/Combat/DamageSources";
|
||||
foreach (var label in new[] { "Attack1", "Attack2", "Attack3" })
|
||||
EditorScaffoldUtils.CreateSOAsset<DamageSourceSO>(dir, $"CMB_Player_{label}");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
// ── SO 资产工厂:玩家(Config 类)────────────────────────────────────────
|
||||
|
||||
private void CreateParryConfig()
|
||||
{
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<ParryConfigSO>(
|
||||
$"{DataRoot}/Player", "PLY_ParryConfig");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateShieldConfig()
|
||||
{
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<ShieldConfigSO>(
|
||||
$"{DataRoot}/Player", "PLY_ShieldConfig");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateEquipmentConfig()
|
||||
{
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<EquipmentConfigSO>(
|
||||
$"{DataRoot}/Player", "PLY_EquipmentConfig");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateCharmCatalog()
|
||||
{
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<CharmCatalogSO>(
|
||||
$"{DataRoot}/Progression/Charms", "PLY_CharmCatalog");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 一键创建全部 Player 所需 SO(已存在则跳过)。
|
||||
/// 完成后提示用户点击"指定所有 SO 到场景角色"完成绑定。
|
||||
/// </summary>
|
||||
private void CreateAllPlayerSOs()
|
||||
{
|
||||
CreatePlayerStat();
|
||||
CreateMovementConfig();
|
||||
CreateAnimConfig();
|
||||
CreateFormConfig();
|
||||
CreateFormWeapons();
|
||||
CreatePlayerDamageSources();
|
||||
CreateParryConfig();
|
||||
CreateShieldConfig();
|
||||
CreateEquipmentConfig();
|
||||
CreateCharmCatalog();
|
||||
AssetDatabase.SaveAssets();
|
||||
EditorUtility.DisplayDialog("创建完成",
|
||||
"全部 Player SO 已创建(已存在的跳过)。\n" +
|
||||
"请在场景中放置角色后,点击「▣ 指定所有 SO 到场景角色」完成绑定。",
|
||||
"确定");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找场景中的 PlayerController,将项目中已存在的配置 SO 全部指定给对应组件字段。
|
||||
/// 使用 SerializedObject 赋值,自动标记 dirty 并保存。
|
||||
/// </summary>
|
||||
private void AssignAllPlayerSOsToScene()
|
||||
{
|
||||
var pc = UnityEngine.Object.FindObjectOfType<PlayerController>();
|
||||
if (pc == null)
|
||||
{
|
||||
EditorUtility.DisplayDialog("未找到角色",
|
||||
"场景中没有 PlayerController。\n请先使用「▣ 放置玩家到场景」。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
var plyGo = pc.gameObject;
|
||||
int count = 0;
|
||||
|
||||
// 通过 SerializedObject 写入 SerializeField(支持撤销)
|
||||
var missing = new System.Collections.Generic.List<string>();
|
||||
|
||||
void TryAssign<T>(Component comp, string field) where T : ScriptableObject
|
||||
{
|
||||
if (comp == null) return;
|
||||
var so = EditorScaffoldUtils.FindAllAssetsOfType<T>().FirstOrDefault();
|
||||
if (so == null)
|
||||
{
|
||||
missing.Add($"{typeof(T).Name}({comp.GetType().Name}.{field})");
|
||||
return;
|
||||
}
|
||||
var sObj = new SerializedObject(comp);
|
||||
var prop = sObj.FindProperty(field);
|
||||
if (prop == null) return;
|
||||
prop.objectReferenceValue = so;
|
||||
sObj.ApplyModifiedProperties();
|
||||
count++;
|
||||
}
|
||||
|
||||
var stats = plyGo.GetComponent<PlayerStats>();
|
||||
var movement = plyGo.GetComponent<PlayerMovement>();
|
||||
var form = plyGo.GetComponent<FormController>();
|
||||
var parry = plyGo.GetComponent<ParrySystem>();
|
||||
var shield = plyGo.GetComponent<ShieldComponent>();
|
||||
var equip = plyGo.GetComponent<EquipmentManager>();
|
||||
var wall = plyGo.GetComponent<PlayerWallDetector>();
|
||||
|
||||
// PlayerStats
|
||||
TryAssign<PlayerStatsSO> (stats, "_config");
|
||||
// PlayerMovement
|
||||
TryAssign<PlayerMovementConfigSO> (movement, "_config");
|
||||
// PlayerController(多个字段)
|
||||
TryAssign<PlayerMovementConfigSO> (pc, "_movementConfig");
|
||||
TryAssign<PlayerAnimationConfigSO> (pc, "_animConfig");
|
||||
TryAssign<InputReaderSO> (pc, "_inputReader");
|
||||
TryAssign<FormConfigSO> (pc, "_formConfig");
|
||||
// FormController
|
||||
TryAssign<FormConfigSO> (form, "_config");
|
||||
// ParrySystem
|
||||
TryAssign<ParryConfigSO> (parry, "_config");
|
||||
// ShieldComponent
|
||||
TryAssign<ShieldConfigSO> (shield, "_config");
|
||||
// EquipmentManager
|
||||
TryAssign<EquipmentConfigSO> (equip, "_config");
|
||||
TryAssign<CharmCatalogSO> (equip, "_charmCatalog");
|
||||
// PlayerWallDetector(复用移动配置)
|
||||
TryAssign<PlayerMovementConfigSO> (wall, "_config");
|
||||
|
||||
EditorUtility.SetDirty(plyGo);
|
||||
RefreshSOStatus();
|
||||
|
||||
string msg = $"已将 {count} 个 SO 引用指定到场景角色 [{plyGo.name}]。";
|
||||
if (missing.Count > 0)
|
||||
msg += $"\n\n★ 以下 SO 尚未创建,请先点击工厂按钮创建后再次指定:\n • " +
|
||||
string.Join("\n • ", missing);
|
||||
else
|
||||
msg += "\n全部 SO 绑定完成,可在 Inspector 中确认各组件字段。";
|
||||
EditorUtility.DisplayDialog("指定完成", msg, "确定");
|
||||
}
|
||||
|
||||
// ── SO 资产工厂:小怪 ────────────────────────────────────────────────
|
||||
|
||||
private void CreateEnemyStat()
|
||||
{
|
||||
string dir = $"{DataRoot}/Enemies/{_enemyId}";
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<EnemyStatsSO>(dir, $"ENM_{_enemyId}_Stats");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateLootTable()
|
||||
{
|
||||
string dir = $"{DataRoot}/Enemies/{_enemyId}";
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<LootTableSO>(dir, $"ENM_{_enemyId}_Loot");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateEnemyAttackPatterns()
|
||||
{
|
||||
string dir = $"{DataRoot}/Enemies/{_enemyId}";
|
||||
foreach (var label in new[] { "Melee", "Ranged" })
|
||||
EditorScaffoldUtils.CreateSOAsset<AttackPatternSO>(dir, $"ENM_{_enemyId}_Pattern_{label}");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateEnemyDamageSource()
|
||||
{
|
||||
string dir = $"{DataRoot}/Enemies/{_enemyId}";
|
||||
EditorScaffoldUtils.CreateSOAsset<DamageSourceSO>(dir, $"ENM_{_enemyId}_DS");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
// ── SO 资产工厂:Boss ─────────────────────────────────────────────────
|
||||
|
||||
private void CreateBossStat()
|
||||
{
|
||||
string dir = $"{DataRoot}/Boss/{_bossId}";
|
||||
EditorScaffoldUtils.CreateSOAsset<EnemyStatsSO>(dir, $"ENM_{_bossId}_Stats");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateBossLoot()
|
||||
{
|
||||
string dir = $"{DataRoot}/Boss/{_bossId}";
|
||||
EditorScaffoldUtils.CreateSOAsset<LootTableSO>(dir, $"ENM_{_bossId}_Loot");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateBossAttackPatterns()
|
||||
{
|
||||
string dir = $"{DataRoot}/Boss/{_bossId}/Patterns";
|
||||
foreach (var label in new[] { "Phase1", "Phase2_A", "Phase2_B" })
|
||||
EditorScaffoldUtils.CreateSOAsset<AttackPatternSO>(dir, $"ENM_{_bossId}_Pattern_{label}");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateBossSkills()
|
||||
{
|
||||
string dir = $"{DataRoot}/Boss/{_bossId}/Skills";
|
||||
foreach (var label in new[] { "Skill_Slam", "Skill_Sweep", "Skill_Summon" })
|
||||
EditorScaffoldUtils.CreateSOAsset<BossSkillSO>(dir, $"SKL_{_bossId}_{label}");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateBossSkillSequence(int phase)
|
||||
{
|
||||
string dir = $"{DataRoot}/Boss/{_bossId}/Skills";
|
||||
EditorScaffoldUtils.CreateSOAsset<SkillSequenceSO>(dir, $"SKL_{_bossId}_Phase{phase}_Sequence");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateBossDamageSources()
|
||||
{
|
||||
string dir = $"{DataRoot}/Boss/{_bossId}/DamageSources";
|
||||
foreach (var label in new[] { "Slam", "Sweep", "Projectile" })
|
||||
EditorScaffoldUtils.CreateSOAsset<DamageSourceSO>(dir, $"ENM_{_bossId}_DS_{label}");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
// ── 场景搭建 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void PlaceSelectedEnemyType()
|
||||
{
|
||||
switch (_enemyTypeIndex)
|
||||
{
|
||||
case 0: SceneObjectPlacerTool.PlaceEnemy(); break;
|
||||
case 1: SceneObjectPlacerTool.PlaceEnemy(); break; // 复用,类型通过 SO 区分
|
||||
case 2: SceneObjectPlacerTool.PlaceEnemy(); break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── SO 状态面板刷新 ───────────────────────────────────────────────────
|
||||
|
||||
private void RefreshSOStatus()
|
||||
{
|
||||
_lastRefreshTime = EditorApplication.timeSinceStartup;
|
||||
BuildPlayerStatus();
|
||||
BuildEnemyStatus();
|
||||
BuildBossStatus();
|
||||
}
|
||||
|
||||
private void BuildPlayerStatus()
|
||||
{
|
||||
if (_playerStatusPanel == null) return;
|
||||
_playerStatusPanel.Clear();
|
||||
|
||||
var checks = new (string label, UnityEngine.Object asset)[]
|
||||
{
|
||||
("PlayerStatsSO", FindFirst<PlayerStatsSO>()),
|
||||
("PlayerMovementConfigSO", FindFirst<PlayerMovementConfigSO>()),
|
||||
("PlayerAnimationConfigSO", FindFirst<PlayerAnimationConfigSO>()),
|
||||
("FormConfigSO", FindFirst<FormConfigSO>()),
|
||||
("FormSO(天魂)", FindFormOfType(FormType.TianHun)),
|
||||
("FormSO(地魂)", FindFormOfType(FormType.DiHun)),
|
||||
("FormSO(命魂)", FindFormOfType(FormType.MingHun)),
|
||||
("WeaponSO(≥3)", FindFirstIfCount<WeaponSO>(3)),
|
||||
("ParryConfigSO", FindFirst<ParryConfigSO>()),
|
||||
("ShieldConfigSO", FindFirst<ShieldConfigSO>()),
|
||||
("EquipmentConfigSO", FindFirst<EquipmentConfigSO>()),
|
||||
("CharmCatalogSO", FindFirst<CharmCatalogSO>()),
|
||||
};
|
||||
|
||||
_playerStatusPanel.Add(MakeStatusGrid(checks));
|
||||
}
|
||||
|
||||
private void BuildEnemyStatus()
|
||||
{
|
||||
if (_enemyStatusPanel == null) return;
|
||||
_enemyStatusPanel.Clear();
|
||||
|
||||
string dir = $"{DataRoot}/Enemies/{_enemyId}";
|
||||
var checks = new (string label, UnityEngine.Object asset)[]
|
||||
{
|
||||
("EnemyStatsSO", FindAtPath<EnemyStatsSO>($"{dir}/ENM_{_enemyId}_Stats.asset")),
|
||||
("LootTableSO", FindAtPath<LootTableSO>($"{dir}/ENM_{_enemyId}_Loot.asset")),
|
||||
("AttackPatternSO×2", FindAtPath<AttackPatternSO>($"{dir}/ENM_{_enemyId}_Pattern_Melee.asset")),
|
||||
("DamageSourceSO", FindAtPath<DamageSourceSO>($"{dir}/ENM_{_enemyId}_DS.asset")),
|
||||
};
|
||||
|
||||
_enemyStatusPanel.Add(MakeStatusGrid(checks));
|
||||
}
|
||||
|
||||
private void BuildBossStatus()
|
||||
{
|
||||
if (_bossStatusPanel == null) return;
|
||||
_bossStatusPanel.Clear();
|
||||
|
||||
string dir = $"{DataRoot}/Boss/{_bossId}";
|
||||
var checks = new (string label, UnityEngine.Object asset)[]
|
||||
{
|
||||
("EnemyStatsSO(Boss)", FindAtPath<EnemyStatsSO>($"{dir}/ENM_{_bossId}_Stats.asset")),
|
||||
("LootTableSO", FindAtPath<LootTableSO>($"{dir}/ENM_{_bossId}_Loot.asset")),
|
||||
("AttackPatternSO(Phase1)", FindAtPath<AttackPatternSO>($"{dir}/Patterns/ENM_{_bossId}_Pattern_Phase1.asset")),
|
||||
("BossSkillSO(≥1)", EditorScaffoldUtils.FindAllAssetsOfType<BossSkillSO>()
|
||||
.FirstOrDefault(s => s.name.StartsWith("SKL_" + _bossId, StringComparison.OrdinalIgnoreCase))),
|
||||
("SkillSequenceSO(Phase1)", FindAtPath<SkillSequenceSO>($"{dir}/Skills/SKL_{_bossId}_Phase1_Sequence.asset")),
|
||||
};
|
||||
|
||||
_bossStatusPanel.Add(MakeStatusGrid(checks));
|
||||
}
|
||||
|
||||
// ── 辅助:状态格 ─────────────────────────────────────────────────────
|
||||
|
||||
private static VisualElement MakeStatusGrid((string label, UnityEngine.Object asset)[] items)
|
||||
{
|
||||
var grid = new VisualElement();
|
||||
grid.style.flexDirection = FlexDirection.Row;
|
||||
grid.style.flexWrap = Wrap.Wrap;
|
||||
grid.style.marginBottom = 6;
|
||||
|
||||
foreach (var (label, asset) in items)
|
||||
{
|
||||
bool exists = asset != null;
|
||||
VisualElement chip;
|
||||
if (exists)
|
||||
{
|
||||
var captured = asset;
|
||||
var btn = new Button(() =>
|
||||
{
|
||||
EditorGUIUtility.PingObject(captured);
|
||||
Selection.activeObject = captured;
|
||||
}) { text = $"✔ {label}" };
|
||||
chip = btn;
|
||||
}
|
||||
else
|
||||
{
|
||||
chip = new Label($"✘ {label}");
|
||||
}
|
||||
|
||||
chip.style.marginRight = 6;
|
||||
chip.style.marginBottom = 4;
|
||||
chip.style.paddingLeft = 6;
|
||||
chip.style.paddingRight = 6;
|
||||
chip.style.paddingTop = 2;
|
||||
chip.style.paddingBottom = 2;
|
||||
chip.style.borderTopLeftRadius = 4;
|
||||
chip.style.borderTopRightRadius = 4;
|
||||
chip.style.borderBottomLeftRadius = 4;
|
||||
chip.style.borderBottomRightRadius = 4;
|
||||
|
||||
if (exists)
|
||||
{
|
||||
chip.style.backgroundColor = new Color(0.18f, 0.55f, 0.22f, 0.85f);
|
||||
chip.style.color = new Color(0.85f, 1.00f, 0.85f);
|
||||
}
|
||||
else
|
||||
{
|
||||
chip.style.backgroundColor = new Color(0.65f, 0.35f, 0.05f, 0.85f);
|
||||
chip.style.color = new Color(1.00f, 0.90f, 0.70f);
|
||||
}
|
||||
|
||||
grid.Add(chip);
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
// ── 辅助:UI 组件构建 ─────────────────────────────────────────────────
|
||||
|
||||
private static VisualElement MakeTabContent()
|
||||
{
|
||||
var root = new ScrollView(ScrollViewMode.Vertical);
|
||||
root.AddToClassList("tab-content");
|
||||
root.style.flexGrow = 1;
|
||||
root.contentContainer.style.paddingLeft = 8;
|
||||
root.contentContainer.style.paddingRight = 8;
|
||||
root.contentContainer.style.paddingTop = 8;
|
||||
root.contentContainer.style.paddingBottom = 8;
|
||||
return root;
|
||||
}
|
||||
|
||||
private static Label MakeSectionHeader(string text)
|
||||
{
|
||||
var lbl = new Label(text);
|
||||
lbl.AddToClassList("section-header");
|
||||
return lbl;
|
||||
}
|
||||
|
||||
private static VisualElement MakeActionGroup()
|
||||
{
|
||||
var group = new VisualElement();
|
||||
group.AddToClassList("action-buttons");
|
||||
return group;
|
||||
}
|
||||
|
||||
private static Button MakeFactoryButton(string label, Action onClick)
|
||||
{
|
||||
var btn = new Button(onClick) { text = $"+ {label}" };
|
||||
btn.AddToClassList("wizard-factory-btn");
|
||||
return btn;
|
||||
}
|
||||
|
||||
private static Button MakeSceneButton(string label, Action onClick)
|
||||
{
|
||||
var btn = new Button(onClick) { text = $"▣ {label}" };
|
||||
btn.AddToClassList("wizard-scene-btn");
|
||||
return btn;
|
||||
}
|
||||
|
||||
private static Button MakeJumpButton(string label, Action onClick)
|
||||
{
|
||||
var btn = new Button(onClick) { text = $"⇒ {label}" };
|
||||
btn.AddToClassList("wizard-jump-btn");
|
||||
return btn;
|
||||
}
|
||||
|
||||
private static HelpBox MakeHelpBox(string text)
|
||||
{
|
||||
return new HelpBox(text, HelpBoxMessageType.Info);
|
||||
}
|
||||
|
||||
private static VisualElement MakeSeparator()
|
||||
{
|
||||
var sep = new VisualElement();
|
||||
sep.style.height = 1;
|
||||
sep.style.backgroundColor = new Color(0.3f, 0.3f, 0.3f, 0.6f);
|
||||
sep.style.marginTop = 8;
|
||||
sep.style.marginBottom = 8;
|
||||
return sep;
|
||||
}
|
||||
|
||||
private static VisualElement MakeLabeledTextField(string label, string value, Action<string> onChange)
|
||||
{
|
||||
var row = new VisualElement();
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.marginBottom = 4;
|
||||
row.style.alignItems = Align.Center;
|
||||
|
||||
var lbl = new Label(label) { style = { minWidth = 110, marginRight = 6 } };
|
||||
var field = new TextField { value = value, style = { flexGrow = 1 } };
|
||||
field.RegisterValueChangedCallback(e => onChange(e.newValue));
|
||||
row.Add(lbl);
|
||||
row.Add(field);
|
||||
return row;
|
||||
}
|
||||
|
||||
private void RefreshEnemyTypeButtons(VisualElement tabRoot)
|
||||
{
|
||||
for (int i = 0; i < EnemyTypeLabels.Length; i++)
|
||||
{
|
||||
var btn = tabRoot.Q<Button>($"enemy-type-{i}");
|
||||
btn?.EnableInClassList("type-btn--active", i == _enemyTypeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 资产查找辅助 ──────────────────────────────────────────
|
||||
|
||||
private static T FindFirst<T>() where T : ScriptableObject
|
||||
=> EditorScaffoldUtils.FindAllAssetsOfType<T>().FirstOrDefault();
|
||||
|
||||
/// <summary>返回第一个,仅当总数达到 minCount 时才认为满足。</summary>
|
||||
private static T FindFirstIfCount<T>(int minCount) where T : ScriptableObject
|
||||
{
|
||||
var list = EditorScaffoldUtils.FindAllAssetsOfType<T>();
|
||||
return list.Count >= minCount ? list[0] : null;
|
||||
}
|
||||
|
||||
private static T FindAtPath<T>(string path) where T : UnityEngine.Object
|
||||
=> AssetDatabase.LoadAssetAtPath<T>(path);
|
||||
|
||||
private static FormSO FindFormOfType(FormType type)
|
||||
=> EditorScaffoldUtils.FindAllAssetsOfType<FormSO>()
|
||||
.FirstOrDefault(f => f.formType == type);
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/CharacterWizardWindow.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/CharacterWizardWindow.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 301eee333a6bf174bac93f44362e72bd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -197,11 +197,10 @@ namespace BaseGames.Editor.Combat
|
||||
_detailRoot.Add(btnRow);
|
||||
}
|
||||
|
||||
/// <summary>attack1 → attack2 → attack3 连击链数值横排预览。</summary>
|
||||
/// <summary>连击序列数值横排预览。</summary>
|
||||
private void BuildComboPreview(WeaponSO weapon)
|
||||
{
|
||||
// 只在有至少一个连击数据时显示
|
||||
if (weapon.attack1Source == null && weapon.attack2Source == null && weapon.attack3Source == null)
|
||||
if (weapon.groundComboSteps == null || weapon.groundComboSteps.Length == 0)
|
||||
return;
|
||||
|
||||
var section = new Label("连击链预览") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } };
|
||||
@@ -210,8 +209,11 @@ namespace BaseGames.Editor.Combat
|
||||
var chain = new VisualElement();
|
||||
chain.AddToClassList("stats-preview");
|
||||
|
||||
void AddSegment(string label, ClipTransition clip, DamageSourceSO src, bool addArrow)
|
||||
for (int i = 0; i < weapon.groundComboSteps.Length; i++)
|
||||
{
|
||||
var step = weapon.groundComboSteps[i];
|
||||
bool addArrow = i < weapon.groundComboSteps.Length - 1;
|
||||
|
||||
var cell = new VisualElement
|
||||
{
|
||||
style =
|
||||
@@ -230,24 +232,21 @@ namespace BaseGames.Editor.Combat
|
||||
}
|
||||
};
|
||||
|
||||
// 段名
|
||||
cell.Add(new Label(label)
|
||||
cell.Add(new Label($"攻击{i + 1}")
|
||||
{
|
||||
style = { fontSize = 10, color = new Color(0.65f, 0.65f, 0.65f) }
|
||||
});
|
||||
|
||||
// Clip 名称
|
||||
string clipName = clip?.Clip != null ? clip.Clip.name : "<无动画>";
|
||||
string clipName = step.clip?.Clip != null ? step.clip.Clip.name : "<无动画>";
|
||||
cell.Add(new Label(clipName)
|
||||
{
|
||||
style = { fontSize = 11, unityFontStyleAndWeight = FontStyle.Bold }
|
||||
});
|
||||
|
||||
// 伤害数值
|
||||
if (src != null)
|
||||
if (step.damageSource != null)
|
||||
{
|
||||
int dmg = Mathf.RoundToInt(src.BaseDamage * src.DamageMultiplier);
|
||||
cell.Add(new Label($"伤害 {dmg} [{src.BreakLevel}]")
|
||||
int dmg = Mathf.RoundToInt(step.damageSource.BaseDamage * step.damageSource.DamageMultiplier);
|
||||
cell.Add(new Label($"伤害 {dmg} [{step.damageSource.BreakLevel}]")
|
||||
{
|
||||
style = { fontSize = 10, color = new Color(1f, 0.7f, 0.3f) }
|
||||
});
|
||||
@@ -266,10 +265,6 @@ namespace BaseGames.Editor.Combat
|
||||
chain.Add(new Label("→") { style = { alignSelf = Align.Center, marginLeft = 2, marginRight = 2 } });
|
||||
}
|
||||
|
||||
AddSegment("攻击1", weapon.attack1Clip, weapon.attack1Source, true);
|
||||
AddSegment("攻击2", weapon.attack2Clip, weapon.attack2Source, true);
|
||||
AddSegment("攻击3", weapon.attack3Clip, weapon.attack3Source, false);
|
||||
|
||||
_detailRoot.Add(chain);
|
||||
|
||||
// 追加空中/上/下攻击的简要行
|
||||
@@ -288,9 +283,9 @@ namespace BaseGames.Editor.Combat
|
||||
});
|
||||
}
|
||||
|
||||
ExtraStat("空中", weapon.airAttackSource);
|
||||
ExtraStat("上挑", weapon.upAttackSource);
|
||||
ExtraStat("下砸", weapon.downAttackSource);
|
||||
ExtraStat("空中", weapon.airComboSteps?[0].damageSource);
|
||||
ExtraStat("上劈", weapon.upStep.damageSource);
|
||||
ExtraStat("下劈", weapon.downStep.damageSource);
|
||||
|
||||
if (extraRow.childCount > 0)
|
||||
_detailRoot.Add(extraRow);
|
||||
|
||||
118
Assets/_Game/Scripts/Editor/Combat/WeaponSOEditor.cs
Normal file
118
Assets/_Game/Scripts/Editor/Combat/WeaponSOEditor.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Player;
|
||||
|
||||
namespace BaseGames.Editor.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// WeaponSO 自定义 Inspector:在 hitBoxPrefab 下方提供一键生成按钮。
|
||||
/// 等同于菜单 BaseGames / Create / Weapon HitBox Prefab,但已预填 weaponId 并自动赋值。
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(WeaponSO))]
|
||||
public class WeaponSOEditor : UnityEditor.Editor
|
||||
{
|
||||
private const string OutputFolder = "Assets/_Game/Prefabs/Weapons";
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
DrawDefaultInspector();
|
||||
|
||||
var weapon = (WeaponSO)target;
|
||||
|
||||
EditorGUILayout.Space(6);
|
||||
EditorGUILayout.LabelField("HitBox Prefab 工具", EditorStyles.boldLabel);
|
||||
|
||||
bool hasId = !string.IsNullOrWhiteSpace(weapon.weaponId);
|
||||
bool hasPrefab = weapon.hitBoxPrefab != null;
|
||||
|
||||
if (!hasId)
|
||||
{
|
||||
EditorGUILayout.HelpBox("请先填写 weaponId,再生成 HitBox Prefab。", MessageType.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
string prefabPath = $"{OutputFolder}/WPN_{weapon.weaponId}_HitBox.prefab";
|
||||
EditorGUILayout.HelpBox($"输出路径:{prefabPath}", MessageType.None);
|
||||
|
||||
string btnLabel = hasPrefab ? "重新生成 HitBox Prefab" : "一键生成 HitBox Prefab";
|
||||
if (GUILayout.Button(btnLabel))
|
||||
{
|
||||
if (hasPrefab && !EditorUtility.DisplayDialog(
|
||||
"确认重新生成",
|
||||
$"hitBoxPrefab 已有引用,是否覆盖并重新赋值?\n\n{prefabPath}",
|
||||
"覆盖", "取消"))
|
||||
return;
|
||||
|
||||
var prefab = CreateHitBoxPrefab(weapon.weaponId, prefabPath);
|
||||
if (prefab != null)
|
||||
{
|
||||
Undo.RecordObject(weapon, "Assign HitBox Prefab");
|
||||
weapon.hitBoxPrefab = prefab;
|
||||
EditorUtility.SetDirty(weapon);
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 创建逻辑 ──────────────────────────────────────────────────────────
|
||||
|
||||
private static GameObject CreateHitBoxPrefab(string weaponId, string assetPath)
|
||||
{
|
||||
EditorScaffoldUtils.EnsureFolder(OutputFolder);
|
||||
|
||||
int layer = LayerMask.NameToLayer("PlayerHitBox");
|
||||
if (layer < 0)
|
||||
{
|
||||
Debug.LogWarning("[WeaponSOEditor] 未找到 Physics Layer 'PlayerHitBox',子节点 Layer 将设为 Default。");
|
||||
layer = 0;
|
||||
}
|
||||
|
||||
string prefabName = $"WPN_{weaponId}_HitBox";
|
||||
var root = new GameObject(prefabName);
|
||||
var instance = root.AddComponent<WeaponHitBoxInstance>();
|
||||
var so = new SerializedObject(instance);
|
||||
|
||||
AddHitBoxChild(root, so, "HitBox_Ground", "_hitBoxGround", layer, new Vector2(1f, 0.5f));
|
||||
AddHitBoxChild(root, so, "HitBox_Up", "_hitBoxUp", layer, new Vector2(0.5f, 1f ));
|
||||
AddHitBoxChild(root, so, "HitBox_Down", "_hitBoxDown", layer, new Vector2(1f, 0.5f));
|
||||
AddHitBoxChild(root, so, "HitBox_Air", "_hitBoxAir", layer, new Vector2(0.5f, 1f ));
|
||||
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
var prefab = PrefabUtility.SaveAsPrefabAsset(root, assetPath);
|
||||
Object.DestroyImmediate(root);
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
if (prefab != null)
|
||||
{
|
||||
EditorScaffoldUtils.PingAndSelect(prefab);
|
||||
Debug.Log($"[WeaponSOEditor] 已创建:{assetPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[WeaponSOEditor] Prefab 保存失败:{assetPath}");
|
||||
}
|
||||
|
||||
return prefab;
|
||||
}
|
||||
|
||||
private static void AddHitBoxChild(GameObject root, SerializedObject so,
|
||||
string nodeName, string fieldName,
|
||||
int layer, Vector2 boxSize)
|
||||
{
|
||||
var child = new GameObject(nodeName);
|
||||
child.transform.SetParent(root.transform, false);
|
||||
child.layer = layer;
|
||||
|
||||
var col = child.AddComponent<BoxCollider2D>();
|
||||
col.isTrigger = true;
|
||||
col.size = boxSize;
|
||||
|
||||
var hb = child.AddComponent<HitBox>();
|
||||
var prop = so.FindProperty(fieldName);
|
||||
if (prop != null)
|
||||
prop.objectReferenceValue = hb;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/Combat/WeaponSOEditor.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Combat/WeaponSOEditor.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df9abfb2b89aa244bbcc1f4e62694dd6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -48,6 +48,7 @@ namespace BaseGames.Editor
|
||||
CreateAsset<VoidEventChannelSO> ("Combat", "EVT_PlayerRespawned");
|
||||
CreateAsset<VoidEventChannelSO> ("Combat", "EVT_RespawnStarted");
|
||||
CreateAsset<VoidEventChannelSO> ("Combat", "EVT_RespawnCompleted");
|
||||
CreateAsset<VoidEventChannelSO> ("Combat", "EVT_CheckpointRespawn");
|
||||
|
||||
// ── Boss ──────────────────────────────────────────────────────────
|
||||
CreateAsset<BossSkillEventChannelSO> ("Boss", "EVT_BossSkill");
|
||||
@@ -70,6 +71,8 @@ namespace BaseGames.Editor
|
||||
|
||||
// ── World ─────────────────────────────────────────────────────────
|
||||
CreateAsset<StringEventChannelSO> ("World", "EVT_SavePointActivated");
|
||||
CreateAsset<VoidEventChannelSO> ("World", "EVT_CheckpointReached");
|
||||
CreateAsset<StringEventChannelSO> ("World", "EVT_DoorOpened"); // 开门/交互机关(钥匙、机关等)触发自动存档
|
||||
|
||||
// ── 对话/商店 ─────────────────────────────────────────────────────
|
||||
CreateAsset<ShopPurchaseEventChannelSO> ("Dialogue", "EVT_ShopPurchase");
|
||||
@@ -92,6 +95,7 @@ namespace BaseGames.Editor
|
||||
// ── 进度/成就 ─────────────────────────────────────────────────────
|
||||
CreateAsset<ToolUsedEventChannelSO> ("Progression", "EVT_ToolUsed");
|
||||
CreateAsset<AchievementEventChannelSO> ("Progression", "EVT_AchievementUnlocked");
|
||||
CreateAsset<StringEventChannelSO> ("Progression", "EVT_MaxHPContainerPickedUp");
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
413
Assets/_Game/Scripts/Editor/FormEditorWindow.cs
Normal file
413
Assets/_Game/Scripts/Editor/FormEditorWindow.cs
Normal file
@@ -0,0 +1,413 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using BaseGames.Player;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 形态系统可视化编辑器(W-06)。
|
||||
/// 技术:UI Toolkit TwoPaneSplitView + 三列形态网格。
|
||||
/// 菜单:BaseGames / Data / Form Editor
|
||||
///
|
||||
/// 左栏:FormConfigSO 列表 + [新建] 按钮。
|
||||
/// 右栏:
|
||||
/// · 三列网格(天魂 / 地魂 / 命魂),每列显示对应 FormSO 详情及武器引用。
|
||||
/// · 各列可独立新建/重新绑定 FormSO。
|
||||
/// · "一键自动填充" 按钮:按 formType 枚举在 Project 中搜索已有 FormSO 并赋值。
|
||||
/// · 底部:在 Project 中选中 FormConfigSO / 在 Inspector 中编辑 原始字段。
|
||||
/// </summary>
|
||||
public class FormEditorWindow : EditorWindow
|
||||
{
|
||||
// ── 常量 ──────────────────────────────────────────────────────────────
|
||||
private const string UssPath = "Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss";
|
||||
private const string DataRoot = "Assets/_Game/Data/Player/Forms";
|
||||
|
||||
private static readonly StyleSheet _uss;
|
||||
private static readonly (FormType type, string label, Color accent)[] FormDefs =
|
||||
{
|
||||
(FormType.TianHun, "天魂", new Color(0.40f, 0.70f, 1.00f)),
|
||||
(FormType.DiHun, "地魂", new Color(0.55f, 0.85f, 0.40f)),
|
||||
(FormType.MingHun, "命魂", new Color(0.80f, 0.30f, 0.30f)),
|
||||
};
|
||||
|
||||
static FormEditorWindow()
|
||||
{
|
||||
_uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Data/Form Editor", priority = 103)]
|
||||
public static void Open()
|
||||
{
|
||||
var wnd = GetWindow<FormEditorWindow>();
|
||||
wnd.titleContent = new GUIContent("Form Editor");
|
||||
wnd.minSize = new Vector2(760, 420);
|
||||
}
|
||||
|
||||
// ── 状态 ──────────────────────────────────────────────────────────────
|
||||
private List<FormConfigSO> _configs = new();
|
||||
private List<FormConfigSO> _filtered = new();
|
||||
private FormConfigSO _selected;
|
||||
private string _searchText = "";
|
||||
|
||||
private ListView _configList;
|
||||
private VisualElement _detailRoot;
|
||||
private InspectorElement _rawInspector;
|
||||
|
||||
// 三列形态格(每列一个 FormSO ObjectField)
|
||||
private readonly ObjectField[] _formFields = new ObjectField[3];
|
||||
private readonly ObjectField[] _weaponFields = new ObjectField[3];
|
||||
private readonly VisualElement[]_columnRoots = new VisualElement[3];
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
|
||||
public void CreateGUI()
|
||||
{
|
||||
if (_uss != null)
|
||||
rootVisualElement.styleSheets.Add(_uss);
|
||||
|
||||
// ── Toolbar ───────────────────────────────────────────────────────
|
||||
var toolbar = new Toolbar();
|
||||
var search = new ToolbarSearchField { style = { flexGrow = 1 } };
|
||||
search.RegisterValueChangedCallback(e => { _searchText = e.newValue; RefreshFilter(); });
|
||||
search.tooltip = "按名称过滤 FormConfigSO";
|
||||
toolbar.Add(search);
|
||||
|
||||
var btnNew = new ToolbarButton(CreateNewFormConfig) { text = "+ 新建 FormConfig" };
|
||||
var btnRefresh = new ToolbarButton(RefreshAll) { text = "↺" };
|
||||
btnRefresh.tooltip = "重新扫描 Project 中的 FormConfigSO 资产";
|
||||
toolbar.Add(btnNew);
|
||||
toolbar.Add(btnRefresh);
|
||||
rootVisualElement.Add(toolbar);
|
||||
|
||||
// ── 分栏 ──────────────────────────────────────────────────────────
|
||||
var split = new TwoPaneSplitView(0, 200, TwoPaneSplitViewOrientation.Horizontal);
|
||||
|
||||
// 左栏
|
||||
var leftPane = new VisualElement { style = { minWidth = 140 } };
|
||||
|
||||
_configList = new ListView
|
||||
{
|
||||
selectionType = SelectionType.Single,
|
||||
makeItem = MakeListItem,
|
||||
bindItem = BindListItem,
|
||||
style = { flexGrow = 1 },
|
||||
showAlternatingRowBackgrounds = AlternatingRowBackground.ContentOnly,
|
||||
};
|
||||
_configList.selectionChanged += OnConfigSelected;
|
||||
leftPane.Add(_configList);
|
||||
split.Add(leftPane);
|
||||
|
||||
// 右栏(ScrollView)
|
||||
_detailRoot = new ScrollView(ScrollViewMode.Vertical) { style = { flexGrow = 1 } };
|
||||
_detailRoot.contentContainer.style.paddingLeft = 10;
|
||||
_detailRoot.contentContainer.style.paddingRight = 10;
|
||||
_detailRoot.contentContainer.style.paddingTop = 10;
|
||||
_detailRoot.contentContainer.style.paddingBottom = 10;
|
||||
_detailRoot.Add(new HelpBox("← 在左侧选择一个 FormConfigSO 开始编辑", HelpBoxMessageType.Info));
|
||||
split.Add(_detailRoot);
|
||||
|
||||
rootVisualElement.Add(split);
|
||||
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
// ── 列表 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private VisualElement MakeListItem()
|
||||
{
|
||||
var label = new Label();
|
||||
label.AddToClassList("list-item");
|
||||
return label;
|
||||
}
|
||||
|
||||
private void BindListItem(VisualElement ve, int idx)
|
||||
{
|
||||
if (ve is Label lbl && idx < _filtered.Count)
|
||||
lbl.text = _filtered[idx].name;
|
||||
}
|
||||
|
||||
private void OnConfigSelected(IEnumerable<object> objs)
|
||||
{
|
||||
_selected = objs.FirstOrDefault() as FormConfigSO;
|
||||
RebuildDetail();
|
||||
}
|
||||
|
||||
private void RefreshFilter()
|
||||
{
|
||||
string s = _searchText.ToLowerInvariant();
|
||||
_filtered = string.IsNullOrEmpty(s)
|
||||
? new List<FormConfigSO>(_configs)
|
||||
: _configs.Where(c => c.name.ToLowerInvariant().Contains(s)).ToList();
|
||||
_configList.itemsSource = _filtered;
|
||||
_configList.Rebuild();
|
||||
}
|
||||
|
||||
private void RefreshAll()
|
||||
{
|
||||
_configs = EditorScaffoldUtils.FindAllAssetsOfType<FormConfigSO>();
|
||||
_configs.Sort((a, b) => string.Compare(a.name, b.name, System.StringComparison.Ordinal));
|
||||
RefreshFilter();
|
||||
}
|
||||
|
||||
// ── 右栏详情 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void RebuildDetail()
|
||||
{
|
||||
_detailRoot.Clear();
|
||||
if (_selected == null) return;
|
||||
|
||||
// 标题 + 快捷操作
|
||||
var header = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginBottom = 8 } };
|
||||
header.Add(new Label(_selected.name) { style = { fontSize = 14, unityFontStyleAndWeight = FontStyle.Bold, flexGrow = 1 } });
|
||||
|
||||
var btnPing = new Button(() => EditorGUIUtility.PingObject(_selected)) { text = "⌖ Ping" };
|
||||
var btnAuto = new Button(AutoFillForms) { text = "自动填充形态", tooltip = "按 formType 枚举在 Project 中搜索已有 FormSO 并赋值到 forms[] 数组" };
|
||||
header.Add(btnAuto);
|
||||
header.Add(btnPing);
|
||||
_detailRoot.Add(header);
|
||||
|
||||
// 三列形态网格
|
||||
var formGrid = new VisualElement();
|
||||
formGrid.style.flexDirection = FlexDirection.Row;
|
||||
formGrid.style.marginBottom = 10;
|
||||
_detailRoot.Add(formGrid);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
formGrid.Add(BuildFormColumn(i));
|
||||
|
||||
// 分割线
|
||||
_detailRoot.Add(MakeSeparator());
|
||||
|
||||
// 原始 Inspector(完整字段编辑)
|
||||
var inspHeader = new Label("原始 Inspector 编辑") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } };
|
||||
_detailRoot.Add(inspHeader);
|
||||
|
||||
_rawInspector = new InspectorElement(_selected);
|
||||
_detailRoot.Add(_rawInspector);
|
||||
}
|
||||
|
||||
private VisualElement BuildFormColumn(int colIdx)
|
||||
{
|
||||
var (formType, label, accent) = FormDefs[colIdx];
|
||||
|
||||
var col = new VisualElement();
|
||||
col.style.flexGrow = 1;
|
||||
col.style.marginRight = colIdx < 2 ? 8 : 0;
|
||||
col.style.borderTopWidth = 2;
|
||||
col.style.borderTopColor = accent;
|
||||
col.style.borderLeftWidth = 1;
|
||||
col.style.borderRightWidth = 1;
|
||||
col.style.borderBottomWidth = 1;
|
||||
col.style.borderLeftColor = new Color(accent.r, accent.g, accent.b, 0.35f);
|
||||
col.style.borderRightColor = new Color(accent.r, accent.g, accent.b, 0.35f);
|
||||
col.style.borderBottomColor = new Color(accent.r, accent.g, accent.b, 0.35f);
|
||||
col.style.borderTopLeftRadius = 4;
|
||||
col.style.borderTopRightRadius = 4;
|
||||
col.style.borderBottomLeftRadius = 4;
|
||||
col.style.borderBottomRightRadius = 4;
|
||||
col.style.paddingLeft = 8;
|
||||
col.style.paddingRight = 8;
|
||||
col.style.paddingTop = 8;
|
||||
col.style.paddingBottom = 8;
|
||||
_columnRoots[colIdx] = col;
|
||||
|
||||
// 列标题 + 色块
|
||||
var titleRow = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginBottom = 6 } };
|
||||
var colorDot = new VisualElement();
|
||||
colorDot.style.width = 12;
|
||||
colorDot.style.height = 12;
|
||||
colorDot.style.borderTopLeftRadius = 6;
|
||||
colorDot.style.borderTopRightRadius = 6;
|
||||
colorDot.style.borderBottomLeftRadius = 6;
|
||||
colorDot.style.borderBottomRightRadius = 6;
|
||||
colorDot.style.backgroundColor = accent;
|
||||
colorDot.style.marginRight = 6;
|
||||
titleRow.Add(colorDot);
|
||||
titleRow.Add(new Label(label) { style = { unityFontStyleAndWeight = FontStyle.Bold } });
|
||||
col.Add(titleRow);
|
||||
|
||||
// 当前 FormSO 显示
|
||||
FormSO current = GetFormByType(formType);
|
||||
|
||||
var formField = new ObjectField("FormSO") { objectType = typeof(FormSO), value = current };
|
||||
formField.RegisterValueChangedCallback(e =>
|
||||
{
|
||||
SetFormByType(formType, e.newValue as FormSO);
|
||||
// 联动刷新武器字段
|
||||
if (_weaponFields[colIdx] != null)
|
||||
_weaponFields[colIdx].value = (e.newValue as FormSO)?.defaultWeapon;
|
||||
});
|
||||
_formFields[colIdx] = formField;
|
||||
col.Add(formField);
|
||||
|
||||
// 武器预览(只读,跟随 FormSO.defaultWeapon)
|
||||
var weaponField = new ObjectField("默认武器") { objectType = typeof(WeaponSO), value = current?.defaultWeapon };
|
||||
weaponField.SetEnabled(false);
|
||||
_weaponFields[colIdx] = weaponField;
|
||||
col.Add(weaponField);
|
||||
|
||||
// 新建该形态 FormSO
|
||||
var btnCreate = new Button(() => CreateFormForType(formType, colIdx))
|
||||
{
|
||||
text = current == null ? $"+ 新建 {label} FormSO" : "重新新建(覆盖)",
|
||||
tooltip = $"在 {DataRoot} 创建 Form_{formType}.asset",
|
||||
style = { marginTop = 8 }
|
||||
};
|
||||
col.Add(btnCreate);
|
||||
|
||||
// 新建该形态 WeaponSO
|
||||
var btnWeapon = new Button(() => CreateWeaponForType(formType, colIdx))
|
||||
{
|
||||
text = $"+ 新建 {label} WeaponSO",
|
||||
tooltip = $"在 Assets/_Game/Data/Player/Weapons 创建 Weapon_{formType}.asset",
|
||||
style = { marginTop = 2 }
|
||||
};
|
||||
col.Add(btnWeapon);
|
||||
|
||||
return col;
|
||||
}
|
||||
|
||||
// ── 操作:新建 FormConfigSO ───────────────────────────────────────────
|
||||
|
||||
private void CreateNewFormConfig()
|
||||
{
|
||||
var cfg = EditorScaffoldUtils.CreateSOAsset<FormConfigSO>(DataRoot, "FormConfig");
|
||||
if (cfg != null)
|
||||
{
|
||||
RefreshAll();
|
||||
// 选中新建项
|
||||
int idx = _filtered.IndexOf(cfg);
|
||||
if (idx >= 0) _configList.SetSelection(idx);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 操作:新建 FormSO ──────────────────────────────────────────────────
|
||||
|
||||
private void CreateFormForType(FormType formType, int colIdx)
|
||||
{
|
||||
if (_selected == null) return;
|
||||
|
||||
EditorScaffoldUtils.EnsureFolder(DataRoot);
|
||||
string path = $"{DataRoot}/Form_{formType}.asset";
|
||||
var form = AssetDatabase.LoadAssetAtPath<FormSO>(path);
|
||||
if (form == null)
|
||||
{
|
||||
form = ScriptableObject.CreateInstance<FormSO>();
|
||||
form.formId = $"Form_{formType}";
|
||||
form.displayName = FormDefs[colIdx].label;
|
||||
form.formType = formType;
|
||||
AssetDatabase.CreateAsset(form, path);
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
|
||||
SetFormByType(formType, form);
|
||||
_formFields[colIdx].value = form;
|
||||
EditorGUIUtility.PingObject(form);
|
||||
}
|
||||
|
||||
// ── 操作:新建 WeaponSO ───────────────────────────────────────────────
|
||||
|
||||
private void CreateWeaponForType(FormType formType, int colIdx)
|
||||
{
|
||||
string dir = "Assets/_Game/Data/Player/Weapons";
|
||||
var weapon = EditorScaffoldUtils.CreateSOAsset<WeaponSO>(dir, $"Weapon_{formType}");
|
||||
if (weapon == null) weapon = AssetDatabase.LoadAssetAtPath<WeaponSO>($"{dir}/Weapon_{formType}.asset");
|
||||
if (weapon == null) return;
|
||||
|
||||
// 赋值到对应 FormSO.defaultWeapon
|
||||
var form = GetFormByType(formType);
|
||||
if (form != null)
|
||||
{
|
||||
form.defaultWeapon = weapon;
|
||||
EditorUtility.SetDirty(form);
|
||||
AssetDatabase.SaveAssets();
|
||||
_weaponFields[colIdx].value = weapon;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 操作:自动填充形态 ────────────────────────────────────────────────
|
||||
|
||||
private void AutoFillForms()
|
||||
{
|
||||
if (_selected == null) return;
|
||||
|
||||
var allForms = EditorScaffoldUtils.FindAllAssetsOfType<FormSO>();
|
||||
bool changed = false;
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var (ftype, _, _) = FormDefs[i];
|
||||
if (GetFormByType(ftype) != null) continue;
|
||||
|
||||
var match = allForms.FirstOrDefault(f => f.formType == ftype);
|
||||
if (match != null)
|
||||
{
|
||||
SetFormByType(ftype, match);
|
||||
_formFields[i].value = match;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
EditorUtility.SetDirty(_selected);
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log("[FormEditorWindow] 自动填充完成。");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("[FormEditorWindow] 未发现需要填充的空槽,或 Project 中无匹配 FormSO。");
|
||||
}
|
||||
}
|
||||
|
||||
// ── 数据访问(操作 FormConfigSO.forms[])────────────────────────────
|
||||
|
||||
private FormSO GetFormByType(FormType type)
|
||||
{
|
||||
if (_selected == null || _selected.forms == null) return null;
|
||||
return _selected.forms.FirstOrDefault(f => f != null && f.formType == type);
|
||||
}
|
||||
|
||||
private void SetFormByType(FormType type, FormSO form)
|
||||
{
|
||||
if (_selected == null) return;
|
||||
|
||||
if (_selected.forms == null || _selected.forms.Length < 3)
|
||||
{
|
||||
var arr = new FormSO[3];
|
||||
if (_selected.forms != null)
|
||||
for (int i = 0; i < _selected.forms.Length && i < 3; i++)
|
||||
arr[i] = _selected.forms[i];
|
||||
_selected.forms = arr;
|
||||
}
|
||||
|
||||
// 按 FormDefs 顺序:TianHun=0, DiHun=1, MingHun=2
|
||||
for (int i = 0; i < FormDefs.Length; i++)
|
||||
{
|
||||
if (FormDefs[i].type == type)
|
||||
{
|
||||
_selected.forms[i] = form;
|
||||
EditorUtility.SetDirty(_selected);
|
||||
AssetDatabase.SaveAssets();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── UI 辅助 ───────────────────────────────────────────────────────────
|
||||
|
||||
private static VisualElement MakeSeparator()
|
||||
{
|
||||
var sep = new VisualElement();
|
||||
sep.style.height = 1;
|
||||
sep.style.backgroundColor = new Color(0.3f, 0.3f, 0.3f, 0.6f);
|
||||
sep.style.marginTop = 10;
|
||||
sep.style.marginBottom = 10;
|
||||
return sep;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/FormEditorWindow.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/FormEditorWindow.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15618e4fc32a98346a68e945428fcb47
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
493
Assets/_Game/Scripts/Editor/GMToolWindow.cs
Normal file
493
Assets/_Game/Scripts/Editor/GMToolWindow.cs
Normal file
@@ -0,0 +1,493 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using BaseGames.Player;
|
||||
using BaseGames.Player.States;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 开发阶段 GM 调试工具窗口(仅 Play Mode 有效)。
|
||||
/// 功能:资源快速填充(灵铢/灵力/魄元)、能力解锁/锁定、形态切换、调试辅助。
|
||||
/// 菜单:BaseGames / Tools / GM Debug Tool
|
||||
/// </summary>
|
||||
public class GMToolWindow : EditorWindow
|
||||
{
|
||||
// ── 菜单 ──────────────────────────────────────────────────────────────
|
||||
[MenuItem("BaseGames/Tools/GM Debug Tool", priority = 2)]
|
||||
public static void Open()
|
||||
{
|
||||
var wnd = GetWindow<GMToolWindow>();
|
||||
wnd.titleContent = new GUIContent("GM Debug Tool");
|
||||
wnd.minSize = new Vector2(320, 500);
|
||||
}
|
||||
|
||||
// ── 资源输入字段 ──────────────────────────────────────────────────────
|
||||
private int _lingZhuAmount = 9999;
|
||||
private int _soulPowerAmount = 100;
|
||||
private int _spiritAmount = 100;
|
||||
|
||||
// ── 折叠状态 ──────────────────────────────────────────────────────────
|
||||
private bool _foldResources = true;
|
||||
private bool _foldJump = true;
|
||||
private bool _foldForms = true;
|
||||
private bool _foldAbilities = false;
|
||||
private bool _foldDebug = true;
|
||||
|
||||
// ── 缓存(避免每帧 FindObjectOfType)─────────────────────────────────
|
||||
private PlayerStats _stats;
|
||||
private FormController _formCtrl;
|
||||
private PlayerController _playerCtrl;
|
||||
private double _lastCacheTime = -10;
|
||||
|
||||
// ── 能力分组定义(与 AbilityTypeDrawer 保持一致)────────────────────
|
||||
private static readonly (string label, AbilityType[] flags)[] AbilityGroups =
|
||||
{
|
||||
("移动能力", new[]
|
||||
{
|
||||
AbilityType.WallCling, AbilityType.WallJump,
|
||||
AbilityType.Dash, AbilityType.Dash,
|
||||
AbilityType.DoubleJump, AbilityType.SuperJump,
|
||||
AbilityType.Swim, AbilityType.Dive,
|
||||
}),
|
||||
("法术能力", new[] { AbilityType.Spell1, AbilityType.Spell2, AbilityType.Spell3 }),
|
||||
("灵魄形态", new[] { AbilityType.SpiritForm, AbilityType.SpiritDash }),
|
||||
("战斗能力", new[] { AbilityType.Parry, AbilityType.ChargeAttack, AbilityType.DownSlash }),
|
||||
("互动能力", new[] { AbilityType.Interact, AbilityType.FastTravel }),
|
||||
("能力强化", new[] { AbilityType.InvincibleDash }),
|
||||
};
|
||||
|
||||
private static readonly string[] AbilityFlagNames =
|
||||
{
|
||||
"贴墙悬挂", "墙跳", "地面冲刺", "空中冲刺", "二段跳", "超级跳", "游泳", "下劈",
|
||||
"法术槽 1", "法术槽 2", "法术槽 3",
|
||||
"灵魄形态", "灵魄冲刺",
|
||||
"弹反", "蓄力攻击", "下斩",
|
||||
"互动", "快速旅行",
|
||||
"无敌冲刺",
|
||||
};
|
||||
|
||||
// ── 样式(懒初始化)──────────────────────────────────────────────────
|
||||
private GUIStyle _headerStyle;
|
||||
private GUIStyle _boxStyle;
|
||||
|
||||
// ── 滚动 ──────────────────────────────────────────────────────────────
|
||||
private Vector2 _scroll;
|
||||
|
||||
// ── EditorWindow 回调 ─────────────────────────────────────────────────
|
||||
|
||||
private void OnEnable() => EditorApplication.playModeStateChanged += OnPlayModeChanged;
|
||||
private void OnDisable() => EditorApplication.playModeStateChanged -= OnPlayModeChanged;
|
||||
|
||||
private void OnPlayModeChanged(PlayModeStateChange state)
|
||||
{
|
||||
_stats = null;
|
||||
_formCtrl = null;
|
||||
_playerCtrl = null;
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EnsureStyles();
|
||||
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
EditorGUILayout.HelpBox("GM 工具仅在 Play Mode 下有效。\n请先运行游戏。", MessageType.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
RefreshCache();
|
||||
|
||||
if (_stats == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox("场景中未找到 PlayerStats 组件。\n请确认玩家已生成。", MessageType.Warning);
|
||||
if (GUILayout.Button("重新扫描")) _lastCacheTime = -10;
|
||||
return;
|
||||
}
|
||||
|
||||
_scroll = EditorGUILayout.BeginScrollView(_scroll);
|
||||
|
||||
DrawResourceSection();
|
||||
DrawJumpSection();
|
||||
DrawFormSection();
|
||||
DrawAbilitySection();
|
||||
DrawDebugSection();
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
// ── 分区:资源 ────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawResourceSection()
|
||||
{
|
||||
_foldResources = DrawFoldout(_foldResources, "资源快速填充");
|
||||
if (!_foldResources) return;
|
||||
|
||||
EditorGUILayout.BeginVertical(_boxStyle);
|
||||
|
||||
// 灵铢
|
||||
EditorGUILayout.LabelField("灵铢", EditorStyles.boldLabel);
|
||||
EditorGUILayout.LabelField($"当前:{_stats.CurrentLingZhu}", EditorStyles.miniLabel);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
_lingZhuAmount = EditorGUILayout.IntField(_lingZhuAmount, GUILayout.Width(80));
|
||||
if (GUILayout.Button("增加")) _stats.AddLingZhu(_lingZhuAmount);
|
||||
if (GUILayout.Button("设为 9999")) _stats.AddLingZhu(Mathf.Max(0, 9999 - _stats.CurrentLingZhu));
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space(6);
|
||||
|
||||
// 灵力(SoulPower)
|
||||
EditorGUILayout.LabelField("灵力(技能用)", EditorStyles.boldLabel);
|
||||
EditorGUILayout.LabelField($"当前:{_stats.CurrentSoulPower} / {_stats.MaxSoulPower}", EditorStyles.miniLabel);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
_soulPowerAmount = EditorGUILayout.IntField(_soulPowerAmount, GUILayout.Width(80));
|
||||
if (GUILayout.Button("增加")) _stats.AddSoulPower(_soulPowerAmount);
|
||||
if (GUILayout.Button("填满")) _stats.AddSoulPower(_stats.MaxSoulPower);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space(6);
|
||||
|
||||
// 魄元(SpiritPower)
|
||||
EditorGUILayout.LabelField("魄元(魄技能用)", EditorStyles.boldLabel);
|
||||
EditorGUILayout.LabelField($"当前:{_stats.CurrentSpiritPower} / {_stats.MaxSpiritPower}", EditorStyles.miniLabel);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
_spiritAmount = EditorGUILayout.IntField(_spiritAmount, GUILayout.Width(80));
|
||||
if (GUILayout.Button("增加")) _stats.AddSpiritPower(_spiritAmount);
|
||||
if (GUILayout.Button("填满")) _stats.AddSpiritPower(_stats.MaxSpiritPower);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
if (GUILayout.Button("▶ 全部资源填满"))
|
||||
{
|
||||
_stats.AddLingZhu(Mathf.Max(0, 9999 - _stats.CurrentLingZhu));
|
||||
_stats.AddSoulPower(_stats.MaxSoulPower);
|
||||
_stats.AddSpiritPower(_stats.MaxSpiritPower);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
}
|
||||
|
||||
// ── 分区:跳跃快捷 ────────────────────────────────────────────────────
|
||||
|
||||
private void DrawJumpSection()
|
||||
{
|
||||
_foldJump = DrawFoldout(_foldJump, "跳跃能力快捷");
|
||||
if (!_foldJump) return;
|
||||
|
||||
EditorGUILayout.BeginVertical(_boxStyle);
|
||||
|
||||
// ── 当前状态 ──
|
||||
bool hasDoubleJump = _stats.HasAbility(AbilityType.DoubleJump);
|
||||
bool hasDash = _stats.HasAbility(AbilityType.Dash);
|
||||
bool hasWallJump = _stats.HasAbility(AbilityType.WallJump);
|
||||
bool hasWallCling = _stats.HasAbility(AbilityType.WallCling);
|
||||
|
||||
int airJumpsLeft = _playerCtrl != null ? _playerCtrl.AirJumpsLeft : -1;
|
||||
int maxAirJumps = _playerCtrl != null && _playerCtrl.MovConfig != null
|
||||
? _playerCtrl.MovConfig.MaxAirJumps : -1;
|
||||
string airStr = airJumpsLeft >= 0
|
||||
? $"{airJumpsLeft} / {maxAirJumps}"
|
||||
: "N/A";
|
||||
|
||||
EditorGUILayout.LabelField(
|
||||
$"二段跳:{(hasDoubleJump ? "✔ 已解锁" : "✘ 未解锁")} | 腾空剩余:{airStr}",
|
||||
EditorStyles.miniLabel);
|
||||
|
||||
// ── MaxAirJumps 控制 ──
|
||||
if (_playerCtrl != null && _playerCtrl.MovConfig != null)
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField("最大空中跳跃次数", GUILayout.Width(120));
|
||||
int newMax = EditorGUILayout.IntSlider(
|
||||
_playerCtrl.MovConfig.MaxAirJumps, 1, 5, GUILayout.ExpandWidth(true));
|
||||
if (newMax != _playerCtrl.MovConfig.MaxAirJumps)
|
||||
{
|
||||
_playerCtrl.MovConfig.MaxAirJumps = newMax;
|
||||
EditorUtility.SetDirty(_playerCtrl.MovConfig);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
EditorGUILayout.LabelField(
|
||||
$" 1=二段跳 2=三段跳 3=四段跳…(需先解锁 DoubleJump 能力)",
|
||||
EditorStyles.miniLabel);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
// ── 跳跃系列快捷按钮(每行 2 个)──
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
DrawToggleAbilityBtn(hasDoubleJump, AbilityType.DoubleJump, "二段跳");
|
||||
DrawToggleAbilityBtn(hasDash, AbilityType.Dash, "冲刺(地面+空中)");
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
DrawToggleAbilityBtn(hasWallJump, AbilityType.WallJump, "墙跳");
|
||||
DrawToggleAbilityBtn(hasWallCling, AbilityType.WallCling, "贴墙悬挂");
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
DrawToggleAbilityBtn(_stats.HasAbility(AbilityType.SuperJump), AbilityType.SuperJump, "超级跳");
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
// ── 批量快捷 ──
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("解锁全部移动能力"))
|
||||
{
|
||||
foreach (var f in new[]
|
||||
{
|
||||
AbilityType.Dash,
|
||||
AbilityType.DoubleJump, AbilityType.SuperJump,
|
||||
AbilityType.WallCling, AbilityType.WallJump,
|
||||
})
|
||||
_stats.UnlockAbility(f);
|
||||
}
|
||||
if (GUILayout.Button("锁定全部移动能力"))
|
||||
{
|
||||
foreach (var f in new[]
|
||||
{
|
||||
AbilityType.Dash,
|
||||
AbilityType.DoubleJump, AbilityType.SuperJump,
|
||||
AbilityType.WallCling, AbilityType.WallJump,
|
||||
})
|
||||
_stats.LockAbility(f);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.HelpBox(
|
||||
"MaxAirJumps 修改立即写入 ScriptableObject(持久化)。\n" +
|
||||
"AirJumpsLeft 在角色下次落地时按新值重置。",
|
||||
MessageType.None);
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绘制一个「已解锁 → 锁定 / 未解锁 → 解锁」的切换按钮。
|
||||
/// </summary>
|
||||
private void DrawToggleAbilityBtn(bool hasIt, AbilityType flag, string label)
|
||||
{
|
||||
GUI.backgroundColor = hasIt ? new Color(0.6f, 1.0f, 0.6f) : new Color(1.0f, 0.85f, 0.6f);
|
||||
string btnText = hasIt ? $"✔ {label}" : $"✘ {label}";
|
||||
if (GUILayout.Button(btnText))
|
||||
{
|
||||
if (hasIt) _stats.LockAbility(flag);
|
||||
else _stats.UnlockAbility(flag);
|
||||
}
|
||||
GUI.backgroundColor = Color.white;
|
||||
}
|
||||
|
||||
// ── 分区:形态 ────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawFormSection()
|
||||
{
|
||||
_foldForms = DrawFoldout(_foldForms, "形态快速切换");
|
||||
if (!_foldForms) return;
|
||||
|
||||
EditorGUILayout.BeginVertical(_boxStyle);
|
||||
|
||||
if (_formCtrl == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox("场景中未找到 FormController 组件。", MessageType.Warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
string cur = _formCtrl.CurrentForm != null ? _formCtrl.CurrentForm.displayName : "未知";
|
||||
EditorGUILayout.LabelField($"当前形态:{cur}", EditorStyles.boldLabel);
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("天魂")) SwitchForm(FormType.TianHun);
|
||||
if (GUILayout.Button("地魂")) SwitchForm(FormType.DiHun);
|
||||
if (GUILayout.Button("命魂")) SwitchForm(FormType.MingHun);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.HelpBox("提示:切换形态前请确保已解锁灵魄形态(SpiritForm)能力。", MessageType.None);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
}
|
||||
|
||||
private void SwitchForm(FormType type)
|
||||
{
|
||||
// 确保 SpiritForm 能力已解锁(否则 FSM 可能拒绝形态切换)
|
||||
_stats.UnlockAbility(AbilityType.SpiritForm);
|
||||
_formCtrl.SwitchForm(type);
|
||||
}
|
||||
|
||||
// ── 分区:能力 ────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawAbilitySection()
|
||||
{
|
||||
_foldAbilities = DrawFoldout(_foldAbilities, "能力解锁 / 锁定");
|
||||
if (!_foldAbilities) return;
|
||||
|
||||
EditorGUILayout.BeginVertical(_boxStyle);
|
||||
|
||||
// 快捷全选/全清
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("全部解锁"))
|
||||
{
|
||||
foreach (var (_, flags) in AbilityGroups)
|
||||
foreach (var f in flags)
|
||||
_stats.UnlockAbility(f);
|
||||
}
|
||||
if (GUILayout.Button("全部锁定"))
|
||||
{
|
||||
foreach (var (_, flags) in AbilityGroups)
|
||||
foreach (var f in flags)
|
||||
_stats.LockAbility(f);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space(6);
|
||||
|
||||
// 各分组
|
||||
foreach (var (groupLabel, flags) in AbilityGroups)
|
||||
{
|
||||
EditorGUILayout.LabelField(groupLabel, EditorStyles.boldLabel);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
bool allOn = true;
|
||||
foreach (var f in flags) if (!_stats.HasAbility(f)) { allOn = false; break; }
|
||||
if (GUILayout.Button(allOn ? "全锁" : "全解", GUILayout.Width(42)))
|
||||
{
|
||||
foreach (var f in flags)
|
||||
{
|
||||
if (allOn) _stats.LockAbility(f);
|
||||
else _stats.UnlockAbility(f);
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
int col = 0;
|
||||
foreach (var flag in flags)
|
||||
{
|
||||
bool has = _stats.HasAbility(flag);
|
||||
bool toggled = GUILayout.Toggle(has, FlagDisplayName(flag), GUILayout.Width(128));
|
||||
if (toggled != has)
|
||||
{
|
||||
if (toggled) _stats.UnlockAbility(flag);
|
||||
else _stats.LockAbility(flag);
|
||||
}
|
||||
col++;
|
||||
if (col == 2) { col = 0; EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); }
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
EditorGUILayout.Space(4);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
}
|
||||
|
||||
// ── 分区:调试辅助 ────────────────────────────────────────────────────
|
||||
|
||||
private void DrawDebugSection()
|
||||
{
|
||||
_foldDebug = DrawFoldout(_foldDebug, "调试辅助");
|
||||
if (!_foldDebug) return;
|
||||
|
||||
EditorGUILayout.BeginVertical(_boxStyle);
|
||||
|
||||
// HP
|
||||
EditorGUILayout.LabelField($"HP:{_stats.CurrentHP} / {_stats.MaxHP}", EditorStyles.miniLabel);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("满血")) _stats.FullHeal();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
// 弹簧充能
|
||||
EditorGUILayout.LabelField($"弹力充能:{_stats.CurrentSpringCharges} / {_stats.MaxSpringCharges}", EditorStyles.miniLabel);
|
||||
if (GUILayout.Button("恢复全部弹力充能")) _stats.RestoreSpringCharges();
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
// 无敌模式(God Mode)
|
||||
bool godNow = _stats.IsInvincible; // 仅作参考,GodMode 内部字段
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("开启无敌模式")) _stats.SetGodMode(true);
|
||||
if (GUILayout.Button("关闭无敌模式")) _stats.SetGodMode(false);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
// 一键全开(开发快速进入测试状态)
|
||||
GUI.backgroundColor = new Color(0.7f, 1.0f, 0.7f);
|
||||
if (GUILayout.Button("▶ 一键满状态(资源 + 全能力 + 满血)", GUILayout.Height(32)))
|
||||
{
|
||||
_stats.FullHeal();
|
||||
_stats.RestoreSpringCharges();
|
||||
_stats.AddLingZhu(Mathf.Max(0, 9999 - _stats.CurrentLingZhu));
|
||||
_stats.AddSoulPower(_stats.MaxSoulPower);
|
||||
_stats.AddSpiritPower(_stats.MaxSpiritPower);
|
||||
foreach (var (_, flags) in AbilityGroups)
|
||||
foreach (var f in flags)
|
||||
_stats.UnlockAbility(f);
|
||||
}
|
||||
GUI.backgroundColor = Color.white;
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
}
|
||||
|
||||
// ── 工具方法 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void RefreshCache()
|
||||
{
|
||||
if (EditorApplication.timeSinceStartup - _lastCacheTime < 2.0) return;
|
||||
_lastCacheTime = EditorApplication.timeSinceStartup;
|
||||
_stats = FindObjectOfType<PlayerStats>();
|
||||
_formCtrl = FindObjectOfType<FormController>();
|
||||
_playerCtrl = FindObjectOfType<PlayerController>();
|
||||
}
|
||||
|
||||
private bool DrawFoldout(bool state, string label)
|
||||
{
|
||||
EditorGUILayout.Space(4);
|
||||
bool next = EditorGUILayout.Foldout(state, label, true, _headerStyle);
|
||||
return next;
|
||||
}
|
||||
|
||||
private static string FlagDisplayName(AbilityType flag) => flag switch
|
||||
{
|
||||
AbilityType.WallCling => "贴墙悬挂",
|
||||
AbilityType.WallJump => "墙跳",
|
||||
AbilityType.Dash => "冲刺",
|
||||
AbilityType.DoubleJump => "二段跳",
|
||||
AbilityType.SuperJump => "超级跳",
|
||||
AbilityType.Swim => "游泳",
|
||||
AbilityType.Dive => "下劈",
|
||||
AbilityType.Spell1 => "法术槽 1",
|
||||
AbilityType.Spell2 => "法术槽 2",
|
||||
AbilityType.Spell3 => "法术槽 3",
|
||||
AbilityType.SpiritForm => "灵魄形态",
|
||||
AbilityType.SpiritDash => "灵魄冲刺",
|
||||
AbilityType.Parry => "弹反",
|
||||
AbilityType.ChargeAttack => "蓄力攻击",
|
||||
AbilityType.DownSlash => "下斩",
|
||||
AbilityType.Interact => "互动",
|
||||
AbilityType.FastTravel => "快速旅行",
|
||||
AbilityType.InvincibleDash => "无敌冲刺",
|
||||
_ => flag.ToString(),
|
||||
};
|
||||
|
||||
private void EnsureStyles()
|
||||
{
|
||||
if (_headerStyle != null) return;
|
||||
_headerStyle = new GUIStyle(EditorStyles.foldout)
|
||||
{
|
||||
fontStyle = FontStyle.Bold,
|
||||
fontSize = 12,
|
||||
};
|
||||
_boxStyle = new GUIStyle(EditorStyles.helpBox)
|
||||
{
|
||||
padding = new RectOffset(8, 8, 6, 6),
|
||||
};
|
||||
}
|
||||
|
||||
// ── 自动刷新(每秒重绘以显示最新数值)──────────────────────────────
|
||||
private void OnInspectorUpdate() => Repaint();
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/GMToolWindow.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/GMToolWindow.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fe104ad18cf3df743a6edd48b173115f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
393
Assets/_Game/Scripts/Editor/SOManagerWindow.cs
Normal file
393
Assets/_Game/Scripts/Editor/SOManagerWindow.cs
Normal file
@@ -0,0 +1,393 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// SO 资产总管理窗口 —— 浏览、搜索并在 Project 窗口定位项目中所有 ScriptableObject 资产。
|
||||
///
|
||||
/// 布局:顶部搜索栏 | 左侧分类列表 | 右侧资产列表(名称 / 类型 / 路径)
|
||||
/// 功能:单击资产行 → Project 窗口 Ping 并选中;双击 → 同上并聚焦 Project 窗口。
|
||||
/// 菜单:BaseGames / Tools / SO Manager (Priority 2)
|
||||
/// </summary>
|
||||
public class SOManagerWindow : EditorWindow
|
||||
{
|
||||
private const string DataRoot = "Assets/_Game/Data";
|
||||
private const string UssPath = "Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss";
|
||||
|
||||
// ── 数据模型 ──────────────────────────────────────────────────────────
|
||||
|
||||
private sealed class CategoryEntry
|
||||
{
|
||||
public string Label;
|
||||
public string Folder; // null = 全部
|
||||
public int Count;
|
||||
}
|
||||
|
||||
private sealed class AssetEntry
|
||||
{
|
||||
public string Name;
|
||||
public string TypeName;
|
||||
public string AssetPath;
|
||||
public ScriptableObject Asset;
|
||||
}
|
||||
|
||||
private readonly List<CategoryEntry> _categories = new();
|
||||
private readonly List<AssetEntry> _allAssets = new();
|
||||
private readonly List<AssetEntry> _filtered = new();
|
||||
|
||||
private int _selectedCatIdx = 0;
|
||||
private string _search = "";
|
||||
|
||||
// ── UI 引用 ───────────────────────────────────────────────────────────
|
||||
|
||||
private ListView _catList;
|
||||
private ListView _assetList;
|
||||
private TextField _searchField;
|
||||
private Label _statusLabel;
|
||||
|
||||
// ── 菜单入口 ──────────────────────────────────────────────────────────
|
||||
|
||||
[MenuItem("BaseGames/Tools/SO Manager", priority = 2)]
|
||||
public static void Open()
|
||||
{
|
||||
var wnd = GetWindow<SOManagerWindow>();
|
||||
wnd.titleContent = new GUIContent("SO Manager",
|
||||
EditorGUIUtility.IconContent("d_ScriptableObject Icon").image);
|
||||
wnd.minSize = new Vector2(680, 420);
|
||||
}
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
|
||||
public void CreateGUI()
|
||||
{
|
||||
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
|
||||
if (uss != null) rootVisualElement.styleSheets.Add(uss);
|
||||
|
||||
BuildUI();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void OnFocus() => Refresh();
|
||||
|
||||
// ── UI 构建 ───────────────────────────────────────────────────────────
|
||||
|
||||
private void BuildUI()
|
||||
{
|
||||
rootVisualElement.style.flexDirection = FlexDirection.Column;
|
||||
|
||||
// ─ 顶部工具栏 ──────────────────────────────────────────────────────
|
||||
var toolbar = new VisualElement();
|
||||
toolbar.style.flexDirection = FlexDirection.Row;
|
||||
toolbar.style.paddingLeft = 8;
|
||||
toolbar.style.paddingRight = 8;
|
||||
toolbar.style.paddingTop = 5;
|
||||
toolbar.style.paddingBottom = 5;
|
||||
toolbar.style.borderBottomWidth = 1;
|
||||
toolbar.style.borderBottomColor = new Color(0.15f, 0.15f, 0.15f);
|
||||
toolbar.style.backgroundColor = new Color(0.22f, 0.22f, 0.22f, 0.6f);
|
||||
|
||||
var searchLbl = new Label("搜索:");
|
||||
searchLbl.style.unityTextAlign = TextAnchor.MiddleLeft;
|
||||
searchLbl.style.marginRight = 4;
|
||||
|
||||
_searchField = new TextField();
|
||||
_searchField.style.flexGrow = 1;
|
||||
_searchField.RegisterValueChangedCallback(e =>
|
||||
{
|
||||
_search = e.newValue;
|
||||
ApplyFilter();
|
||||
});
|
||||
|
||||
var refreshBtn = new Button(Refresh) { text = "↻ 刷新" };
|
||||
refreshBtn.style.marginLeft = 8;
|
||||
refreshBtn.style.width = 58;
|
||||
|
||||
toolbar.Add(searchLbl);
|
||||
toolbar.Add(_searchField);
|
||||
toolbar.Add(refreshBtn);
|
||||
rootVisualElement.Add(toolbar);
|
||||
|
||||
// ─ 主体:两栏分割 ──────────────────────────────────────────────────
|
||||
var split = new TwoPaneSplitView(0, 164, TwoPaneSplitViewOrientation.Horizontal);
|
||||
split.style.flexGrow = 1;
|
||||
|
||||
// 左栏:分类列表 ────────────────────────────────────────────────────
|
||||
var leftPane = new VisualElement();
|
||||
leftPane.style.flexDirection = FlexDirection.Column;
|
||||
leftPane.style.minWidth = 100;
|
||||
|
||||
var catHeader = new Label("分类");
|
||||
catHeader.style.paddingLeft = 8;
|
||||
catHeader.style.paddingTop = 5;
|
||||
catHeader.style.paddingBottom = 5;
|
||||
catHeader.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
catHeader.style.borderBottomWidth = 1;
|
||||
catHeader.style.borderBottomColor = new Color(0.22f, 0.22f, 0.22f);
|
||||
catHeader.style.backgroundColor = new Color(0.22f, 0.22f, 0.22f, 0.4f);
|
||||
leftPane.Add(catHeader);
|
||||
|
||||
_catList = new ListView
|
||||
{
|
||||
makeItem = MakeCatItem,
|
||||
bindItem = BindCatItem,
|
||||
selectionType = SelectionType.Single,
|
||||
fixedItemHeight = 26,
|
||||
};
|
||||
_catList.style.flexGrow = 1;
|
||||
_catList.selectionChanged += _ =>
|
||||
{
|
||||
if (_catList.selectedIndex >= 0)
|
||||
{
|
||||
_selectedCatIdx = _catList.selectedIndex;
|
||||
ApplyFilter();
|
||||
}
|
||||
};
|
||||
leftPane.Add(_catList);
|
||||
|
||||
// 右栏:资产列表 ────────────────────────────────────────────────────
|
||||
var rightPane = new VisualElement();
|
||||
rightPane.style.flexDirection = FlexDirection.Column;
|
||||
|
||||
// 列标题行
|
||||
var colHeader = new VisualElement();
|
||||
colHeader.style.flexDirection = FlexDirection.Row;
|
||||
colHeader.style.paddingLeft = 8;
|
||||
colHeader.style.paddingRight = 8;
|
||||
colHeader.style.paddingTop = 4;
|
||||
colHeader.style.paddingBottom = 4;
|
||||
colHeader.style.borderBottomWidth = 1;
|
||||
colHeader.style.borderBottomColor = new Color(0.22f, 0.22f, 0.22f);
|
||||
colHeader.style.backgroundColor = new Color(0.22f, 0.22f, 0.22f, 0.4f);
|
||||
colHeader.Add(MakeHeaderLabel("资产名", true, 0));
|
||||
colHeader.Add(MakeHeaderLabel("类型", false, 170));
|
||||
colHeader.Add(MakeHeaderLabel("路径", true, 0));
|
||||
rightPane.Add(colHeader);
|
||||
|
||||
_assetList = new ListView
|
||||
{
|
||||
makeItem = MakeAssetRow,
|
||||
bindItem = BindAssetRow,
|
||||
selectionType = SelectionType.Single,
|
||||
fixedItemHeight = 22,
|
||||
};
|
||||
_assetList.style.flexGrow = 1;
|
||||
_assetList.selectionChanged += _ => OnAssetPicked();
|
||||
_assetList.itemsChosen += _ => FocusProjectWindow();
|
||||
rightPane.Add(_assetList);
|
||||
|
||||
// 状态栏
|
||||
_statusLabel = new Label("—");
|
||||
_statusLabel.style.paddingLeft = 8;
|
||||
_statusLabel.style.paddingTop = 3;
|
||||
_statusLabel.style.paddingBottom = 3;
|
||||
_statusLabel.style.borderTopWidth = 1;
|
||||
_statusLabel.style.borderTopColor = new Color(0.15f, 0.15f, 0.15f);
|
||||
_statusLabel.style.color = new Color(0.58f, 0.58f, 0.58f);
|
||||
_statusLabel.style.fontSize = 11;
|
||||
rightPane.Add(_statusLabel);
|
||||
|
||||
split.Add(leftPane);
|
||||
split.Add(rightPane);
|
||||
rootVisualElement.Add(split);
|
||||
}
|
||||
|
||||
private static Label MakeHeaderLabel(string text, bool grow, int fixedWidth)
|
||||
{
|
||||
var lbl = new Label(text);
|
||||
lbl.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
lbl.style.overflow = Overflow.Hidden;
|
||||
if (grow) lbl.style.flexGrow = 1;
|
||||
if (fixedWidth > 0) lbl.style.width = fixedWidth;
|
||||
return lbl;
|
||||
}
|
||||
|
||||
// ── 分类列表项 ────────────────────────────────────────────────────────
|
||||
|
||||
private static VisualElement MakeCatItem()
|
||||
{
|
||||
var lbl = new Label();
|
||||
lbl.style.paddingLeft = 10;
|
||||
lbl.style.paddingRight = 6;
|
||||
lbl.style.overflow = Overflow.Hidden;
|
||||
lbl.style.unityTextAlign = TextAnchor.MiddleLeft;
|
||||
return lbl;
|
||||
}
|
||||
|
||||
private void BindCatItem(VisualElement el, int i)
|
||||
{
|
||||
if (i >= _categories.Count) return;
|
||||
var cat = _categories[i];
|
||||
((Label)el).text = $"{cat.Label} ({cat.Count})";
|
||||
}
|
||||
|
||||
// ── 资产列表项 ────────────────────────────────────────────────────────
|
||||
|
||||
private static VisualElement MakeAssetRow()
|
||||
{
|
||||
var row = new VisualElement();
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.alignItems = Align.Center;
|
||||
row.style.paddingLeft = 8;
|
||||
row.style.paddingRight = 8;
|
||||
|
||||
var nameEl = new Label { name = "n" };
|
||||
nameEl.style.flexGrow = 1;
|
||||
nameEl.style.overflow = Overflow.Hidden;
|
||||
nameEl.style.textOverflow = TextOverflow.Ellipsis;
|
||||
|
||||
var typeEl = new Label { name = "t" };
|
||||
typeEl.style.width = 170;
|
||||
typeEl.style.overflow = Overflow.Hidden;
|
||||
typeEl.style.textOverflow = TextOverflow.Ellipsis;
|
||||
typeEl.style.color = new Color(0.52f, 0.80f, 1.00f);
|
||||
typeEl.style.fontSize = 11;
|
||||
|
||||
var pathEl = new Label { name = "p" };
|
||||
pathEl.style.flexGrow = 1;
|
||||
pathEl.style.overflow = Overflow.Hidden;
|
||||
pathEl.style.textOverflow = TextOverflow.Ellipsis;
|
||||
pathEl.style.color = new Color(0.48f, 0.48f, 0.48f);
|
||||
pathEl.style.fontSize = 10;
|
||||
|
||||
row.Add(nameEl);
|
||||
row.Add(typeEl);
|
||||
row.Add(pathEl);
|
||||
return row;
|
||||
}
|
||||
|
||||
private void BindAssetRow(VisualElement el, int i)
|
||||
{
|
||||
if (i >= _filtered.Count) return;
|
||||
var e = _filtered[i];
|
||||
el.Q<Label>("n").text = e.Name;
|
||||
el.Q<Label>("t").text = e.TypeName;
|
||||
// 显示相对于 DataRoot 的路径,去掉文件名本身只保留目录
|
||||
string rel = e.AssetPath.StartsWith(DataRoot + "/")
|
||||
? e.AssetPath.Substring(DataRoot.Length + 1)
|
||||
: e.AssetPath;
|
||||
// 去掉最后的文件名,只显示目录部分
|
||||
string dir = Path.GetDirectoryName(rel)?.Replace('\\', '/') ?? "";
|
||||
el.Q<Label>("p").text = string.IsNullOrEmpty(dir) ? "/" : dir;
|
||||
}
|
||||
|
||||
// ── 资产选中 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void OnAssetPicked()
|
||||
{
|
||||
int idx = _assetList.selectedIndex;
|
||||
if (idx < 0 || idx >= _filtered.Count) return;
|
||||
var asset = _filtered[idx].Asset;
|
||||
if (asset == null) return;
|
||||
EditorGUIUtility.PingObject(asset);
|
||||
Selection.activeObject = asset;
|
||||
}
|
||||
|
||||
private static void FocusProjectWindow()
|
||||
{
|
||||
EditorApplication.ExecuteMenuItem("Window/General/Project");
|
||||
}
|
||||
|
||||
// ── 数据逻辑 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
_allAssets.Clear();
|
||||
|
||||
// 扫描 DataRoot 下所有 ScriptableObject 资产
|
||||
var guids = AssetDatabase.FindAssets("t:ScriptableObject", new[] { DataRoot });
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<ScriptableObject>(path);
|
||||
if (asset == null) continue;
|
||||
_allAssets.Add(new AssetEntry
|
||||
{
|
||||
Name = asset.name,
|
||||
TypeName = asset.GetType().Name,
|
||||
AssetPath = path,
|
||||
Asset = asset,
|
||||
});
|
||||
}
|
||||
|
||||
// 构建分类:首项为"全部",其余为 DataRoot 的直接子目录
|
||||
_categories.Clear();
|
||||
_categories.Add(new CategoryEntry
|
||||
{
|
||||
Label = "全部",
|
||||
Folder = null,
|
||||
Count = _allAssets.Count,
|
||||
});
|
||||
|
||||
if (AssetDatabase.IsValidFolder(DataRoot))
|
||||
{
|
||||
foreach (var sub in AssetDatabase.GetSubFolders(DataRoot).OrderBy(f => f))
|
||||
{
|
||||
string folderName = Path.GetFileName(sub);
|
||||
int count = _allAssets.Count(a =>
|
||||
a.AssetPath.StartsWith(sub + "/", StringComparison.Ordinal));
|
||||
if (count == 0) continue;
|
||||
_categories.Add(new CategoryEntry
|
||||
{
|
||||
Label = folderName,
|
||||
Folder = sub,
|
||||
Count = count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_catList.itemsSource = _categories;
|
||||
_catList.Rebuild();
|
||||
|
||||
int clampedIdx = Mathf.Clamp(_selectedCatIdx, 0, _categories.Count - 1);
|
||||
_catList.SetSelection(clampedIdx);
|
||||
_selectedCatIdx = clampedIdx;
|
||||
ApplyFilter();
|
||||
}
|
||||
|
||||
private void ApplyFilter()
|
||||
{
|
||||
_filtered.Clear();
|
||||
|
||||
string folder = (_selectedCatIdx >= 0 && _selectedCatIdx < _categories.Count)
|
||||
? _categories[_selectedCatIdx].Folder
|
||||
: null;
|
||||
|
||||
IEnumerable<AssetEntry> source = folder == null
|
||||
? _allAssets
|
||||
: _allAssets.Where(a => a.AssetPath.StartsWith(folder + "/", StringComparison.Ordinal));
|
||||
|
||||
foreach (var entry in source)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_search)
|
||||
|| entry.Name.IndexOf(_search, StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| entry.TypeName.IndexOf(_search, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
_filtered.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// 同分类内按类型分组,再按名称排序
|
||||
_filtered.Sort((a, b) =>
|
||||
{
|
||||
int cmp = string.Compare(a.TypeName, b.TypeName, StringComparison.OrdinalIgnoreCase);
|
||||
return cmp != 0 ? cmp : string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
|
||||
_assetList.itemsSource = _filtered;
|
||||
_assetList.Rebuild();
|
||||
|
||||
int total = folder == null
|
||||
? _allAssets.Count
|
||||
: _allAssets.Count(a => a.AssetPath.StartsWith(folder + "/", StringComparison.Ordinal));
|
||||
_statusLabel.text = string.IsNullOrEmpty(_search)
|
||||
? $"共 {_filtered.Count} 个资产"
|
||||
: $"筛选 {_filtered.Count} / {total} 个资产(搜索:{_search})";
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/SOManagerWindow.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/SOManagerWindow.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7dd063f0750f2c24cae7c29f40b24a8a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,11 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Animancer;
|
||||
using BaseGames.Camera;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Combat.StatusEffects;
|
||||
using BaseGames.Dialogue;
|
||||
using BaseGames.Enemies;
|
||||
using BaseGames.Equipment;
|
||||
using BaseGames.Parry;
|
||||
using BaseGames.Player;
|
||||
using BaseGames.Player.States;
|
||||
using BaseGames.Skills;
|
||||
using BaseGames.World;
|
||||
using PathBerserker2d;
|
||||
using Unity.Cinemachine;
|
||||
@@ -33,53 +38,83 @@ namespace BaseGames.Editor
|
||||
{
|
||||
var report = new List<string>();
|
||||
|
||||
GameObject go = new GameObject("Player");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place Player");
|
||||
go.transform.position = GetDropPosition();
|
||||
go.tag = "Player";
|
||||
SetLayer(go, "Player", report);
|
||||
// ── Player 根节点(行为+物理+标签三合一)──────────────────────────────
|
||||
// Rigidbody2D / 所有 MonoBehaviour 集中于此节点。
|
||||
// HurtBox 作为其子节点,GetComponentInParent<IDamageable>() 向上即可找到
|
||||
// 本节点上的 PlayerController(IDamageable 实现者)。
|
||||
GameObject root = new GameObject("Player");
|
||||
Undo.RegisterCreatedObjectUndo(root, "Place Player");
|
||||
root.transform.position = GetDropPosition();
|
||||
root.tag = "Player";
|
||||
SetLayer(root, "Player", report);
|
||||
|
||||
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
|
||||
// 物理组件(PlayerMovement RequireComponent(Rigidbody2D),必须同节点)
|
||||
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(root);
|
||||
rb.bodyType = RigidbodyType2D.Dynamic;
|
||||
rb.gravityScale = 2f;
|
||||
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
||||
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||||
rb.interpolation = RigidbodyInterpolation2D.Interpolate;
|
||||
GetOrAddComponent<BoxCollider2D>(root);
|
||||
|
||||
GetOrAddComponent<CapsuleCollider2D>(go);
|
||||
GetOrAddComponent<Animator>(go);
|
||||
SetupSpriteRenderer(go);
|
||||
// 动画组件(AnimancerComponent 需要 Animator 存在;PlayerController
|
||||
// [RequireComponent(typeof(AnimancerComponent))] 保证其存在)
|
||||
GetOrAddComponent<Animator>(root);
|
||||
GetOrAddComponent<AnimancerComponent>(root);
|
||||
|
||||
PlayerStats playerStats = GetOrAddComponent<PlayerStats>(go);
|
||||
PlayerMovement playerMovement = GetOrAddComponent<PlayerMovement>(go);
|
||||
PlayerController playerController = GetOrAddComponent<PlayerController>(go);
|
||||
PlayerCombat playerCombat = GetOrAddComponent<PlayerCombat>(go);
|
||||
SetupSpriteRenderer(root);
|
||||
|
||||
// Ground check pivot
|
||||
Transform groundCheckGo = GetOrCreateChild(go.transform, "GroundCheck");
|
||||
groundCheckGo.localPosition = new Vector3(0f, -0.75f, 0f);
|
||||
AssignReference(playerMovement, "_groundCheck", groundCheckGo, report);
|
||||
AssignLayerMask(playerMovement, "_groundLayer", "Ground", report);
|
||||
// 核心行为组件
|
||||
PlayerStats playerStats = GetOrAddComponent<PlayerStats>(root);
|
||||
PlayerMovement playerMovement = GetOrAddComponent<PlayerMovement>(root);
|
||||
PlayerCombat playerCombat = GetOrAddComponent<PlayerCombat>(root);
|
||||
FormController formController = GetOrAddComponent<FormController>(root);
|
||||
WeaponManager weaponManager = GetOrAddComponent<WeaponManager>(root);
|
||||
SkillManager skillManager = GetOrAddComponent<SkillManager>(root);
|
||||
SpringSystem springSystem = GetOrAddComponent<SpringSystem>(root);
|
||||
ParrySystem parrySystem = GetOrAddComponent<ParrySystem>(root);
|
||||
ShieldComponent shield = GetOrAddComponent<ShieldComponent>(root);
|
||||
PlayerWallDetector wallDetector = GetOrAddComponent<PlayerWallDetector>(root);
|
||||
EquipmentManager equipmentManager = GetOrAddComponent<EquipmentManager>(root);
|
||||
GetOrAddComponent<SkillModifierRegistry>(root);
|
||||
GetOrAddComponent<StatusEffectManager>(root);
|
||||
// PlayerController 最后添加:RequireComponent 会拉取上方已加好的组件
|
||||
PlayerController playerController = GetOrAddComponent<PlayerController>(root);
|
||||
|
||||
// Weapon socket (WeaponManager instantiates weapons here at runtime)
|
||||
GetOrCreateChild(go.transform, "WeaponSocket");
|
||||
|
||||
// Camera follow target — CinemachineCamera.Follow 使用此子节点而非 Player 根节点
|
||||
GetOrCreateChild(go.transform, "CameraFollowTarget");
|
||||
|
||||
// HurtBox child
|
||||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||||
// ── HurtBox 子节点 ───────────────────────────────────────────────────
|
||||
Transform hurtBoxT = GetOrCreateChild(root.transform, "HurtBox");
|
||||
SetLayer(hurtBoxT.gameObject, "PlayerHurtBox", report);
|
||||
CapsuleCollider2D hurtCollider = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
|
||||
BoxCollider2D hurtCollider = GetOrAddComponent<BoxCollider2D>(hurtBoxT.gameObject);
|
||||
hurtCollider.isTrigger = true;
|
||||
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
|
||||
|
||||
// Assign controller references
|
||||
AssignReference(playerController, "_stats", playerStats, report);
|
||||
AssignReference(playerController, "_hurtBox", hurtBox, report);
|
||||
AssignReference(playerController, "_movement", playerMovement, report);
|
||||
AssignReference(playerController, "_combat", playerCombat, report);
|
||||
// ── [WeaponSocket] 子节点(WeaponManager 动态实例化武器 HitBox 的挂点)
|
||||
GetOrCreateChild(root.transform, "[WeaponSocket]");
|
||||
|
||||
// Event channels (all optional — will be skipped silently if assets missing)
|
||||
// ── GroundCheck 子节点(地面检测 Transform)────────────────────────
|
||||
Transform groundCheckT = GetOrCreateChild(root.transform, "GroundCheck");
|
||||
groundCheckT.localPosition = new Vector3(0f, -0.75f, 0f);
|
||||
AssignReference(playerMovement, "_groundCheck", groundCheckT, report);
|
||||
AssignLayerMask(playerMovement, "_groundLayer", "Ground", report);
|
||||
|
||||
// ── SkillHitBox_Slot 子节点(技能 HitBox 实例化挂点)────────────────
|
||||
GetOrCreateChild(root.transform, "SkillHitBox_Slot");
|
||||
|
||||
// ── CameraFollowTarget 子节点(CinemachineCamera.Follow 目标)────────
|
||||
GetOrCreateChild(root.transform, "CameraFollowTarget");
|
||||
|
||||
// ── PlayerController SerializeField 引用赋值 ──────────────────────
|
||||
AssignReference(playerController, "_combat", playerCombat, report);
|
||||
AssignReference(playerController, "_formController", formController, report);
|
||||
AssignReference(playerController, "_weaponManager", weaponManager, report);
|
||||
AssignReference(playerController, "_skillManager", skillManager, report);
|
||||
AssignReference(playerController, "_springSystem", springSystem, report);
|
||||
AssignReference(playerController, "_parrySystem", parrySystem, report);
|
||||
AssignReference(playerController, "_hurtBox", hurtBox, report);
|
||||
AssignReference(playerController, "_shield", shield, report);
|
||||
AssignReference(playerController, "_wallDetector", wallDetector, report);
|
||||
|
||||
// ── 事件频道(可选,缺失时跳过) ───────────────────────────────────
|
||||
AssignAsset(playerStats, "_onHPChanged", report, false, "EVT_HPChanged");
|
||||
AssignAsset(playerStats, "_onMaxHPChanged", report, false, "EVT_MaxHPChanged");
|
||||
AssignAsset(playerStats, "_onSoulPowerChanged", report, false, "EVT_SoulPowerChanged");
|
||||
@@ -93,17 +128,43 @@ namespace BaseGames.Editor
|
||||
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
|
||||
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
|
||||
|
||||
// Config ScriptableObjects (optional — link manually after placing)
|
||||
Object statsConfig = FindFirstAsset("PLY_PlayerStats", "PlayerStats");
|
||||
Object movConfig = FindFirstAsset("PLY_PlayerMovementConfig", "PlayerMovementConfig");
|
||||
if (movConfig != null) AssignReference(playerController, "_movementConfig", movConfig, report);
|
||||
// ── Config SO 自动查找(资产存在时自动绑定)──────────────────────
|
||||
Object statsConfig = FindFirstAsset("PLY_PlayerStats");
|
||||
Object movConfig = FindFirstAsset("PLY_PlayerMovementConfig");
|
||||
Object formConfig = FindFirstAsset("PLY_FormConfig");
|
||||
Object parryConfig = FindFirstAsset("PLY_ParryConfig");
|
||||
Object shieldConfig = FindFirstAsset("PLY_ShieldConfig");
|
||||
Object inputReader = FindFirstAsset("InputReader");
|
||||
Object equipmentConfig = FindFirstAsset("PLY_EquipmentConfig");
|
||||
Object charmCatalog = FindFirstAsset("PLY_CharmCatalog");
|
||||
|
||||
if (statsConfig != null) AssignReference(playerStats, "_config", statsConfig, report);
|
||||
if (movConfig != null) AssignReference(playerMovement, "_config", movConfig, report);
|
||||
if (movConfig != null)
|
||||
{
|
||||
AssignReference(playerController, "_movementConfig", movConfig, report);
|
||||
AssignReference(playerMovement, "_config", movConfig, report);
|
||||
AssignReference(wallDetector, "_config", movConfig, report);
|
||||
}
|
||||
if (formConfig != null)
|
||||
{
|
||||
AssignReference(playerController, "_formConfig", formConfig, report);
|
||||
AssignReference(formController, "_config", formConfig, report);
|
||||
}
|
||||
if (parryConfig != null) AssignReference(parrySystem, "_config", parryConfig, report);
|
||||
if (shieldConfig != null) AssignReference(shield, "_config", shieldConfig, report);
|
||||
if (inputReader != null) AssignReference(playerController, "_inputReader", inputReader, report);
|
||||
if (equipmentConfig != null) AssignReference(equipmentManager, "_config", equipmentConfig, report);
|
||||
if (charmCatalog != null) AssignReference(equipmentManager, "_charmCatalog", charmCatalog, report);
|
||||
|
||||
report.Add("PlayerMovement._config、PlayerController._animConfig、_inputReader 等需后续手动绑定。");
|
||||
report.Add("★ 需手动绑定:PlayerController._animConfig(PLY_PlayerAnimationConfig)");
|
||||
if (statsConfig == null) report.Add("★ 需创建并绑定:PlayerStats._config(PlayerStatsSO)");
|
||||
if (inputReader == null) report.Add("★ 需手动绑定:PlayerController._inputReader(InputReaderSO)");
|
||||
if (equipmentConfig == null) report.Add("★ 需创建并绑定:EquipmentManager._config(EquipmentConfigSO)");
|
||||
if (charmCatalog == null) report.Add("★ 需创建并绑定:EquipmentManager._charmCatalog(CharmCatalogSO)");
|
||||
report.Add("SkillManager 技能槽 SO 需手动填入。");
|
||||
|
||||
Selection.activeGameObject = go;
|
||||
MarkDirtyAndLog("Player", go, report);
|
||||
Selection.activeGameObject = root;
|
||||
MarkDirtyAndLog("Player", root, report);
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Scene/Place/Player Spawn Point", priority = 105)]
|
||||
@@ -168,11 +229,11 @@ namespace BaseGames.Editor
|
||||
AssignReference(enemyBase, "_stats", enemyStats, report);
|
||||
|
||||
// DamageSourceSO for body contact (optional — create manually if missing)
|
||||
Object dmgSrc = FindFirstAsset("DS_EnemyBody", "DS_TestEnemyBody");
|
||||
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody", "DS_EnemyBody");
|
||||
if (dmgSrc != null)
|
||||
AssignReference(hitBox, "_defaultSource", dmgSrc, report);
|
||||
else
|
||||
report.Add("未找到 DamageSourceSO (DS_EnemyBody),HitBox_Body._defaultSource 未绑定。请创建后手动指定。");
|
||||
report.Add("未找到 DamageSourceSO,HitBox_Body._defaultSource 未绑定。请按规范创建 CMB_DS_EnemyBody.asset。");
|
||||
|
||||
// Event channels
|
||||
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
|
||||
@@ -186,7 +247,7 @@ namespace BaseGames.Editor
|
||||
if (enemyStatsSO != null)
|
||||
AssignReference(enemyBase, "_statsSO", enemyStatsSO, report);
|
||||
else
|
||||
report.Add("未找到 EnemyStatsSO,EnemyBase._statsSO 未绑定。请在 Data/Enemies/ 创建后手动指定。");
|
||||
report.Add("未找到 EnemyStatsSO,EnemyBase._statsSO 未绑定。请在 Data/Enemies/ 创建 ENM_{id}_Stats.asset 后手动指定。");
|
||||
|
||||
report.Add("行为树、导航参数(NavAgent)、动画片段需后续手工挂载。");
|
||||
|
||||
@@ -239,11 +300,11 @@ namespace BaseGames.Editor
|
||||
AssignReference(bossBase, "_stats", bossStats, report);
|
||||
|
||||
// DamageSourceSO
|
||||
Object dmgSrc = FindFirstAsset("DS_BossBody", "DS_EnemyBody");
|
||||
Object dmgSrc = FindFirstAsset("CMB_DS_BossBody", "CMB_DS_EnemyBody", "DS_BossBody");
|
||||
if (dmgSrc != null)
|
||||
AssignReference(hitBox, "_defaultSource", dmgSrc, report);
|
||||
else
|
||||
report.Add("未找到 DamageSourceSO,HitBox_Body._defaultSource 未绑定。");
|
||||
report.Add("未找到 DamageSourceSO,HitBox_Body._defaultSource 未绑定。请按规范创建 CMB_DS_BossBody.asset。");
|
||||
|
||||
// Event channels
|
||||
AssignAsset(bossBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
|
||||
@@ -291,6 +352,7 @@ namespace BaseGames.Editor
|
||||
GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
|
||||
|
||||
AssignAsset(trap, "_onPlayerDied", report, false, "EVT_PlayerDied");
|
||||
AssignAsset(trap, "_onCheckpointRespawn", report, false, "EVT_CheckpointRespawn");
|
||||
|
||||
report.Add("_canPogo=true:子 HurtBox 供玩家下劈弹起;设为 false 可改为纯死亡区(无需子 HurtBox)。");
|
||||
|
||||
@@ -298,7 +360,32 @@ namespace BaseGames.Editor
|
||||
MarkDirtyAndLog("Hazard (LethalTrap)", go, report);
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Scene/Place/Collectible (LingZhu)", priority = 125)]
|
||||
[MenuItem("BaseGames/Scene/Place/Checkpoint Marker", priority = 125)]
|
||||
public static void PlaceCheckpointMarker()
|
||||
{
|
||||
var report = new List<string>();
|
||||
|
||||
GameObject go = new GameObject("CheckpointMarker");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place CheckpointMarker");
|
||||
go.transform.position = GetDropPosition();
|
||||
SetLayer(go, "TriggerZone", report);
|
||||
|
||||
BoxCollider2D col = GetOrAddComponent<BoxCollider2D>(go);
|
||||
col.isTrigger = true;
|
||||
col.size = new Vector2(1f, 2f);
|
||||
|
||||
CheckpointMarker marker = GetOrAddComponent<CheckpointMarker>(go);
|
||||
AssignLayerMask(marker, "_playerLayers", "Player", report);
|
||||
AssignAsset(marker, "_onCheckpointReached", report, false, "EVT_CheckpointReached");
|
||||
|
||||
report.Add("放置于跳跳乐段落的关键节点处;玩家经过后成为该房间最近检查点。");
|
||||
report.Add("同一房间可放置多个,以最近经过的为准。");
|
||||
|
||||
Selection.activeGameObject = go;
|
||||
MarkDirtyAndLog("Checkpoint Marker", go, report);
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Scene/Place/Collectible (LingZhu)", priority = 130)]
|
||||
public static void PlaceCollectible()
|
||||
{
|
||||
var report = new List<string>();
|
||||
@@ -349,14 +436,41 @@ namespace BaseGames.Editor
|
||||
|
||||
SavePoint savePoint = GetOrAddComponent<SavePoint>(go);
|
||||
|
||||
AssignAsset(savePoint, "_onSceneLoaded", report, false, "EVT_SceneLoaded");
|
||||
AssignAsset(savePoint, "_onSavePointActivated", report, false, "EVT_SavePointActivated");
|
||||
AssignAsset(savePoint, "_onFastTravelOpen", report, false, "EVT_FastTravelOpen");
|
||||
|
||||
report.Add("填写 _savePointId(全局唯一字符串,用于存档点激活记录与复活定位)。");
|
||||
|
||||
Selection.activeGameObject = go;
|
||||
MarkDirtyAndLog("Save Point", go, report);
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Scene/Place/Room Transition", priority = 135)]
|
||||
[MenuItem("BaseGames/Scene/Place/Teleport Station", priority = 135)]
|
||||
public static void PlaceTeleportStation()
|
||||
{
|
||||
var report = new List<string>();
|
||||
|
||||
GameObject go = new GameObject("TeleportStation");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place TeleportStation");
|
||||
go.transform.position = GetDropPosition();
|
||||
SetLayer(go, "TriggerZone", report);
|
||||
|
||||
BoxCollider2D col = GetOrAddComponent<BoxCollider2D>(go);
|
||||
col.isTrigger = true;
|
||||
col.size = new Vector2(1.5f, 2f);
|
||||
SetupSpriteRenderer(go);
|
||||
|
||||
TeleportStation station = GetOrAddComponent<TeleportStation>(go);
|
||||
AssignAsset(station, "_onFastTravelOpen", report, false, "EVT_FastTravelOpen");
|
||||
|
||||
report.Add("填写 _stationId(传送站唯一 ID,用于地图 UI 标注)。");
|
||||
report.Add("传送站不存档、不复活、不恢复 HP;与存档点是独立对象。");
|
||||
|
||||
Selection.activeGameObject = go;
|
||||
MarkDirtyAndLog("Teleport Station", go, report);
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Scene/Place/Room Transition", priority = 140)]
|
||||
public static void PlaceRoomTransition()
|
||||
{
|
||||
var report = new List<string>();
|
||||
@@ -410,20 +524,12 @@ namespace BaseGames.Editor
|
||||
|
||||
CameraArea cameraArea = GetOrAddComponent<CameraArea>(go);
|
||||
|
||||
// AreaBoundary child — 提供 CinemachineConfiner2D 所需的限位多边形(isTrigger = true,仅作为相机约束边界)
|
||||
// AreaBoundary child — 提供 CinemachineConfiner3D 所需的限位体积
|
||||
Transform boundaryT = GetOrCreateChild(go.transform, $"{areaName}_AreaBoundary");
|
||||
PolygonCollider2D boundaryCollider = GetOrAddComponent<PolygonCollider2D>(boundaryT.gameObject);
|
||||
BoxCollider boundaryCollider = GetOrAddComponent<BoxCollider>(boundaryT.gameObject);
|
||||
boundaryCollider.isTrigger = true;
|
||||
boundaryCollider.pathCount = 1;
|
||||
// 顶点必须逆时针(CCW)排列:Cinemachine 底层 Clipper 库对 CW 多边形(area<0)会取反 delta,
|
||||
// 导致向外膨胀而非向内收缩,相机将不受限制地跑出边界。
|
||||
boundaryCollider.SetPath(0, new Vector2[]
|
||||
{
|
||||
new Vector2(-12f, -6f), // BL
|
||||
new Vector2( 12f, -6f), // BR
|
||||
new Vector2( 12f, 6f), // TR
|
||||
new Vector2(-12f, 6f), // TL
|
||||
});
|
||||
boundaryCollider.center = new Vector3(0f, 0f, -10f); // Z 占位符,实际深度由 SyncConfiner 按 LensConfig 计算
|
||||
boundaryCollider.size = new Vector3(24f, 12f, 1f); // 默认房间尺寸占位符
|
||||
|
||||
AssignReference(cameraArea, "_confinerCollider", boundaryCollider, report);
|
||||
|
||||
@@ -433,26 +539,26 @@ namespace BaseGames.Editor
|
||||
zoneGo.transform.position = pos;
|
||||
SetLayer(zoneGo, "TriggerZone", report);
|
||||
|
||||
CameraTriggerZone zone = GetOrAddComponent<CameraTriggerZone>(zoneGo);
|
||||
PolygonCollider2D col = GetOrAddComponent<PolygonCollider2D>(zoneGo);
|
||||
col.isTrigger = true;
|
||||
// 默认矩形轮廓(CCW),与 AreaBoundary 默认尺寸一致(可在 Inspector 中编辑顶点调整为任意多边形)
|
||||
// 默认矩形多边形(24×12),可在 Inspector 中编辑顶点
|
||||
col.SetPath(0, new Vector2[]
|
||||
{
|
||||
new Vector2(-12f, -6f), // BL
|
||||
new Vector2( 12f, -6f), // BR
|
||||
new Vector2( 12f, 6f), // TR
|
||||
new Vector2(-12f, 6f), // TL
|
||||
new Vector2(-12f, -6f),
|
||||
new Vector2(-12f, 6f),
|
||||
new Vector2( 12f, 6f),
|
||||
new Vector2( 12f, -6f),
|
||||
});
|
||||
|
||||
CameraTriggerZone zone = GetOrAddComponent<CameraTriggerZone>(zoneGo);
|
||||
AssignReference(zone, "_targetArea", cameraArea, report);
|
||||
// TriggerZone 归入 CameraArea 节点,方便统一调整与查找
|
||||
Undo.SetTransformParent(zoneGo.transform, go.transform, "Parent TriggerZone to CameraArea");
|
||||
zoneGo.transform.localPosition = Vector3.zero;
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
|
||||
report.Add($"调整 {areaName}_AreaBoundary PolygonCollider2D 顶点以匹配区域边界。");
|
||||
report.Add($"调整 {areaName}_TriggerZone PolygonCollider2D 顶点以匹配入口走廊(支持任意多边形)。");
|
||||
report.Add($"绑定 LensConfig SO 后单击 Inspector 中「从可视区域更新限位区域」计算 {areaName}_AreaBoundary BoxCollider。");
|
||||
report.Add($"编辑 {areaName}_TriggerZone PolygonCollider2D 的顶点以匹配入口多边形区域。");
|
||||
|
||||
// ── 自动关联到同场景 RoomController(若其 _cameraArea 为空)────────
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
|
||||
@@ -92,7 +92,7 @@ namespace BaseGames.Editor
|
||||
|
||||
GameObject vcamAGo = GetOrCreateChild(camera, "VCamA").gameObject;
|
||||
CinemachineCamera vcamA = GetOrAddComponent<CinemachineCamera>(vcamAGo);
|
||||
GetOrAddComponent<CinemachineConfiner2D>(vcamAGo);
|
||||
GetOrAddComponent<CinemachineConfiner3D>(vcamAGo);
|
||||
GetOrAddComponent<CameraAxisLockExtension>(vcamAGo);
|
||||
GetOrAddComponent<CameraAsymmetricDampingExtension>(vcamAGo);
|
||||
GetOrAddComponent<CameraAdaptiveLookaheadExtension>(vcamAGo);
|
||||
@@ -102,7 +102,7 @@ namespace BaseGames.Editor
|
||||
|
||||
GameObject vcamBGo = GetOrCreateChild(camera, "VCamB").gameObject;
|
||||
CinemachineCamera vcamB = GetOrAddComponent<CinemachineCamera>(vcamBGo);
|
||||
GetOrAddComponent<CinemachineConfiner2D>(vcamBGo);
|
||||
GetOrAddComponent<CinemachineConfiner3D>(vcamBGo);
|
||||
GetOrAddComponent<CameraAxisLockExtension>(vcamBGo);
|
||||
GetOrAddComponent<CameraAsymmetricDampingExtension>(vcamBGo);
|
||||
GetOrAddComponent<CameraAdaptiveLookaheadExtension>(vcamBGo);
|
||||
@@ -221,15 +221,11 @@ namespace BaseGames.Editor
|
||||
GameObject cameraAreaGo = GetOrCreateChild(cameraGroup, "CameraArea").gameObject;
|
||||
CameraArea cameraArea = GetOrAddComponent<CameraArea>(cameraAreaGo);
|
||||
|
||||
// AreaBoundary — 提供 CinemachineConfiner2D 所需的限位多边形
|
||||
// AreaBoundary — 提供 CinemachineConfiner3D 所需的限位体积
|
||||
Transform boundaryT = GetOrCreateChild(cameraAreaGo.transform, "AreaBoundary");
|
||||
PolygonCollider2D boundaryCollider = GetOrAddComponent<PolygonCollider2D>(boundaryT.gameObject);
|
||||
boundaryCollider.pathCount = 1;
|
||||
boundaryCollider.SetPath(0, new Vector2[]
|
||||
{
|
||||
new Vector2(-12f, -6f), new Vector2(-12f, 6f),
|
||||
new Vector2( 12f, 6f), new Vector2( 12f, -6f),
|
||||
});
|
||||
BoxCollider boundaryCollider = GetOrAddComponent<BoxCollider>(boundaryT.gameObject);
|
||||
boundaryCollider.center = new Vector3(0f, 0f, -10f); // Z 占位符,实际深度由 SyncConfiner 按 LensConfig 计算
|
||||
boundaryCollider.size = new Vector3(24f, 12f, 1f); // 默认房间尺寸占位符
|
||||
|
||||
AssignReference(cameraArea, "_confinerCollider", boundaryCollider);
|
||||
|
||||
@@ -282,7 +278,7 @@ namespace BaseGames.Editor
|
||||
|
||||
// ── Report ─────────────────────────────────────────────────────
|
||||
report.Add("在 RoomController._roomId 填写唯一房间 ID(如 \"Room_Forest_01\")。");
|
||||
report.Add("调整 AreaBoundary PolygonCollider2D 顶点以匹配实际房间大小。");
|
||||
report.Add("绑定 LensConfig SO 后单击 Inspector 中「从可视区域更新限位区域」计算正确的 BoxCollider。");
|
||||
report.Add("使用 Tile Palette 在 Ground Tilemap 上绘制地形,然后在 NavSurface Inspector 中点击 Bake。");
|
||||
report.Add("[Transitions] 子节点下使用 BaseGames/Scene/Place/Room Transition 添加过渡点。");
|
||||
|
||||
|
||||
@@ -76,3 +76,79 @@
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-color: rgb(100, 160, 255);
|
||||
}
|
||||
|
||||
/* ── CharacterWizardWindow 专用类 ───────────────────────── */
|
||||
|
||||
/* 向导标签页按钮(复用 tab-bar 容器) */
|
||||
.tab-btn {
|
||||
flex-grow: 1;
|
||||
padding: 6px 0;
|
||||
border-radius: 0;
|
||||
border-width: 0;
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-color: rgba(0,0,0,0);
|
||||
background-color: rgba(0,0,0,0);
|
||||
color: rgb(170, 170, 170);
|
||||
-unity-font-style: bold;
|
||||
font-size: 13px;
|
||||
}
|
||||
.tab-btn:hover {
|
||||
background-color: rgba(255,255,255,0.06);
|
||||
}
|
||||
.tab-btn--active {
|
||||
color: rgb(255, 255, 255);
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-color: rgb(90, 160, 255);
|
||||
}
|
||||
|
||||
/* SO 工厂按钮(绿调) */
|
||||
.wizard-factory-btn {
|
||||
background-color: rgba(40, 100, 55, 0.75);
|
||||
color: rgb(200, 255, 210);
|
||||
border-radius: 4px;
|
||||
padding: 3px 10px;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
border-width: 1px;
|
||||
border-color: rgba(80, 180, 100, 0.60);
|
||||
}
|
||||
.wizard-factory-btn:hover {
|
||||
background-color: rgba(55, 130, 70, 0.90);
|
||||
}
|
||||
|
||||
/* 场景放置按钮(蓝调) */
|
||||
.wizard-scene-btn {
|
||||
background-color: rgba(30, 60, 120, 0.80);
|
||||
color: rgb(190, 220, 255);
|
||||
border-radius: 4px;
|
||||
padding: 3px 10px;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
border-width: 1px;
|
||||
border-color: rgba(80, 130, 220, 0.55);
|
||||
}
|
||||
.wizard-scene-btn:hover {
|
||||
background-color: rgba(40, 80, 155, 0.95);
|
||||
}
|
||||
|
||||
/* 跳转按钮(中性灰调) */
|
||||
.wizard-jump-btn {
|
||||
background-color: rgba(55, 55, 60, 0.80);
|
||||
color: rgb(210, 210, 220);
|
||||
border-radius: 4px;
|
||||
padding: 3px 10px;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
border-width: 1px;
|
||||
border-color: rgba(120, 120, 130, 0.45);
|
||||
}
|
||||
.wizard-jump-btn:hover {
|
||||
background-color: rgba(75, 75, 85, 0.95);
|
||||
}
|
||||
|
||||
/* 小怪类型选择按钮 */
|
||||
.type-btn--active {
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-color: rgb(255, 200, 50);
|
||||
color: rgb(255, 220, 100);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,14 @@ namespace BaseGames.Input
|
||||
private float _attackBuffer;
|
||||
private float _dashBuffer;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// ── 运行时调试(Inspector 中可见)───────────────────────────────
|
||||
[Header("\u2500\u2500 \u8fd0\u884c\u65f6\u8c03\u8bd5 \u2500\u2500")]
|
||||
[SerializeField] private float _dbg_JumpBuffer;
|
||||
[SerializeField] private float _dbg_AttackBuffer;
|
||||
[SerializeField] private float _dbg_DashBuffer;
|
||||
#endif
|
||||
|
||||
// ── Named handlers to allow proper unsubscription ─────────────────────
|
||||
private void HandleJumpStarted() => _jumpBuffer = _jumpBufferDuration;
|
||||
private void HandleAttackStarted() => _attackBuffer = _attackBufferDuration;
|
||||
@@ -51,6 +59,12 @@ namespace BaseGames.Input
|
||||
_jumpBuffer = Mathf.Max(0f, _jumpBuffer - dt);
|
||||
_attackBuffer = Mathf.Max(0f, _attackBuffer - dt);
|
||||
_dashBuffer = Mathf.Max(0f, _dashBuffer - dt);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
_dbg_JumpBuffer = _jumpBuffer;
|
||||
_dbg_AttackBuffer = _attackBuffer;
|
||||
_dbg_DashBuffer = _dashBuffer;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>消耗跳跃缓冲(读取并清空)。</summary>
|
||||
|
||||
@@ -17,8 +17,6 @@ namespace BaseGames.Input
|
||||
public event Action JumpStartedEvent;
|
||||
public event Action JumpCancelledEvent;
|
||||
public event Action AttackEvent;
|
||||
public event Action DownAttackEvent;
|
||||
public event Action UpAttackEvent;
|
||||
public event Action ParryEvent;
|
||||
public event Action DashEvent;
|
||||
public event Action UseSpringEvent;
|
||||
@@ -167,8 +165,6 @@ namespace BaseGames.Input
|
||||
BindStarted(_gameplay, "Jump", () => JumpStartedEvent?.Invoke());
|
||||
BindCanceled(_gameplay, "Jump", () => JumpCancelledEvent?.Invoke());
|
||||
BindStarted(_gameplay, "Attack", () => AttackEvent?.Invoke());
|
||||
BindStarted(_gameplay, "DownAttack", () => DownAttackEvent?.Invoke());
|
||||
BindStarted(_gameplay, "UpAttack", () => UpAttackEvent?.Invoke());
|
||||
BindStarted(_gameplay, "Parry", () => ParryEvent?.Invoke());
|
||||
BindStarted(_gameplay, "Dash", () => DashEvent?.Invoke());
|
||||
BindStarted(_gameplay, "UseSpring", () => UseSpringEvent?.Invoke());
|
||||
|
||||
@@ -15,8 +15,7 @@ namespace BaseGames.Player
|
||||
// ── 移动能力 ──────────────────────────────────────────────────────
|
||||
WallCling = 1u << 0, // 贴墙悬挂
|
||||
WallJump = 1u << 1, // 墙跳
|
||||
Dash = 1u << 2, // 地面冲刺
|
||||
AirDash = 1u << 3, // 空中冲刺(二段冲刺)
|
||||
Dash = 1u << 2, // 冲刺(地面与空中统一)
|
||||
DoubleJump = 1u << 4, // 二段跳
|
||||
SuperJump = 1u << 5, // 超级跳(聚气跳)
|
||||
Swim = 1u << 6, // 游泳(液体中自由移动)
|
||||
@@ -44,12 +43,12 @@ namespace BaseGames.Player
|
||||
/// <summary>
|
||||
/// 无敌冲刺强化(解锁后冲刺前段获得无敌窗口)。
|
||||
/// 仅持有 Dash 时:冲刺无无敌帧。
|
||||
/// 解锁 InvincibleDash 后:冲刺期间完全无敌(地面 DashState + 空中 AerialDashState)。
|
||||
/// 解锁 InvincibleDash 后:冲刺期间完全无敌。
|
||||
/// </summary>
|
||||
InvincibleDash = 1u << 18,
|
||||
|
||||
// ── 组合掩码 ─────────────────────────────────────────────────────────
|
||||
AllMovement = WallCling | WallJump | Dash | AirDash | DoubleJump | SuperJump | Swim | Dive | InvincibleDash,
|
||||
AllMovement = WallCling | WallJump | Dash | DoubleJump | SuperJump | Swim | Dive | InvincibleDash,
|
||||
AllSpells = Spell1 | Spell2 | Spell3,
|
||||
AllSpirit = SpiritForm | SpiritDash,
|
||||
}
|
||||
|
||||
62
Assets/_Game/Scripts/Player/CheckpointRespawnHandler.cs
Normal file
62
Assets/_Game/Scripts/Player/CheckpointRespawnHandler.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 监听 EVT_CheckpointRespawn,将玩家瞬移至最近检查点坐标。
|
||||
/// 通过淡出 → 移位 → 淡入实现无视觉跳变的传送。
|
||||
///
|
||||
/// 挂载在玩家根节点(与 Rigidbody2D 同一 GameObject)。
|
||||
/// </summary>
|
||||
public class CheckpointRespawnHandler : MonoBehaviour
|
||||
{
|
||||
[Header("事件 - 监听")]
|
||||
[Tooltip("EVT_CheckpointRespawn — 由 LethalTrap 在玩家存活且场景有检查点时触发")]
|
||||
[SerializeField] private VoidEventChannelSO _onCheckpointRespawn;
|
||||
|
||||
[Header("事件 - 触发")]
|
||||
[Tooltip("EVT_FadeOutRequest — 传送前淡出屏幕")]
|
||||
[SerializeField] private VoidEventChannelSO _onFadeOutRequest;
|
||||
|
||||
[Tooltip("EVT_FadeInRequest — 传送后淡入屏幕")]
|
||||
[SerializeField] private VoidEventChannelSO _onFadeInRequest;
|
||||
|
||||
[Header("配置")]
|
||||
[Tooltip("淡出结束到执行位移之间的等待时长(秒,不受 TimeScale 影响)")]
|
||||
[SerializeField] private float _fadeHalfDuration = 0.2f;
|
||||
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
private void OnEnable() => _onCheckpointRespawn?.Subscribe(OnCheckpointRespawn).AddTo(_subs);
|
||||
private void OnDisable() => _subs.Clear();
|
||||
|
||||
private void OnCheckpointRespawn() => StartCoroutine(RespawnCoroutine());
|
||||
|
||||
private IEnumerator RespawnCoroutine()
|
||||
{
|
||||
_onFadeOutRequest?.Raise();
|
||||
yield return new WaitForSecondsRealtime(_fadeHalfDuration);
|
||||
|
||||
var svc = ServiceLocator.GetOrDefault<ICheckpointService>();
|
||||
if (svc != null && svc.HasCheckpoint)
|
||||
{
|
||||
// 清零速度后移位,防止物理残留动量导致滑步
|
||||
var rb = GetComponent<Rigidbody2D>();
|
||||
if (rb != null)
|
||||
{
|
||||
rb.velocity = Vector2.zero;
|
||||
rb.position = svc.CheckpointPosition;
|
||||
}
|
||||
else
|
||||
{
|
||||
transform.position = svc.CheckpointPosition;
|
||||
}
|
||||
}
|
||||
|
||||
_onFadeInRequest?.Raise();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/CheckpointRespawnHandler.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/CheckpointRespawnHandler.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ca41f67644b6b843ba7ef65e78b13e5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Input;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
@@ -16,6 +17,7 @@ namespace BaseGames.Player
|
||||
{
|
||||
[Header("配置")]
|
||||
[SerializeField] private FormConfigSO _config;
|
||||
[SerializeField] private InputReaderSO _input;
|
||||
|
||||
[Header("事件频道")]
|
||||
[SerializeField] private IntEventChannelSO _onFormChanged; // 广播当前形态索引(UI/Save)
|
||||
@@ -33,6 +35,22 @@ namespace BaseGames.Player
|
||||
Debug.Assert(_config != null, "[FormController] _config 未赋值,请在 Inspector 中指定 FormConfigSO。", this);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_input == null) return;
|
||||
_input.SwitchSkyFormEvent += OnSwitchSky;
|
||||
_input.SwitchEarthFormEvent += OnSwitchEarth;
|
||||
_input.SwitchDeathFormEvent += OnSwitchDeath;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_input == null) return;
|
||||
_input.SwitchSkyFormEvent -= OnSwitchSky;
|
||||
_input.SwitchEarthFormEvent -= OnSwitchEarth;
|
||||
_input.SwitchDeathFormEvent -= OnSwitchDeath;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (_config.forms != null && _config.forms.Length > 0)
|
||||
@@ -67,5 +85,10 @@ namespace BaseGames.Player
|
||||
if (form != null)
|
||||
SwitchForm(form.formType);
|
||||
}
|
||||
|
||||
// ── 内部输入处理 ────────────────────────────────────────────────────────
|
||||
private void OnSwitchSky() => SwitchForm(FormType.TianHun);
|
||||
private void OnSwitchEarth() => SwitchForm(FormType.DiHun);
|
||||
private void OnSwitchDeath() => SwitchForm(FormType.MingHun);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,20 @@ namespace BaseGames.Player
|
||||
public AnimationClip Idle;
|
||||
public AnimationClip Run;
|
||||
public AnimationClip Jump;
|
||||
[Tooltip("空中跳跃(二段跳)动画。留空则复用 Jump 动画。")]
|
||||
public AnimationClip AirJump;
|
||||
public AnimationClip Fall;
|
||||
[Tooltip("普通冲刺动画(无无敌帧,过程中受伤会被打断)。")]
|
||||
public AnimationClip Dash;
|
||||
[Tooltip("无敌冲刺动画(解锁 InvincibleDash 能力后使用)。留空则复用 Dash 动画。")]
|
||||
public AnimationClip DashInvincible;
|
||||
|
||||
[Header("墙")]
|
||||
public AnimationClip WallSlide;
|
||||
[Tooltip("背墙跳动画(无输入或反向输入时触发,远离墙壁斜上方弹出)。留空则复用 Jump 动画。")]
|
||||
public AnimationClip WallJumpAway;
|
||||
[Tooltip("对墙跳动画(朝向墙壁输入时触发,沿墙壁方向斜上方弹出)。留空则复用 Jump 动画。")]
|
||||
public AnimationClip WallJumpToward;
|
||||
|
||||
[Header("受伤 / 死亡")]
|
||||
public AnimationClip Hurt;
|
||||
@@ -24,28 +33,6 @@ namespace BaseGames.Player
|
||||
[Header("弹簧")]
|
||||
public AnimationClip UseSpring;
|
||||
|
||||
[Header("地面攻击(连招序列)")]
|
||||
public AnimationClip[] GroundAttacks;
|
||||
|
||||
/// <summary>每段连击的 HitBox 开启/关闭时间点(归一化 0-1),与 GroundAttacks 索引对应。</summary>
|
||||
[System.Serializable]
|
||||
public struct AttackTimings
|
||||
{
|
||||
[Tooltip("HitBox 开启时间点(归一化 0-1)")]
|
||||
[UnityEngine.Range(0f, 1f)] public float HitBoxEnter;
|
||||
[Tooltip("HitBox 关闭时间点(归一化 0-1)")]
|
||||
[UnityEngine.Range(0f, 1f)] public float HitBoxExit;
|
||||
}
|
||||
|
||||
[Header("地面攻击 HitBox 激活窗口(归一化时间 0-1)")]
|
||||
[Tooltip("每段连击 HitBox 开启/关闭时间点,与 GroundAttacks 索引对应")]
|
||||
public AttackTimings[] GroundAttackTimings = { new AttackTimings { HitBoxEnter = 0.3f, HitBoxExit = 0.6f } };
|
||||
|
||||
[Header("空中攻击")]
|
||||
public AnimationClip AirAttack;
|
||||
public AnimationClip UpAttack;
|
||||
public AnimationClip DownAttack; // 戳击 (Pogo)
|
||||
|
||||
[Header("弹反")]
|
||||
public AnimationClip ParryStart;
|
||||
public AnimationClip ParrySuccess;
|
||||
@@ -53,12 +40,5 @@ namespace BaseGames.Player
|
||||
[Header("游泳")]
|
||||
public AnimationClip SwimIdle;
|
||||
public AnimationClip SwimMove;
|
||||
|
||||
/// <summary>按连招步骤取地面攻击动画,越界自动取最后一个。</summary>
|
||||
public AnimationClip GetAttackClip(int step)
|
||||
{
|
||||
if (GroundAttacks == null || GroundAttacks.Length == 0) return null;
|
||||
return step < GroundAttacks.Length ? GroundAttacks[step] : GroundAttacks[^1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace BaseGames.Player
|
||||
[SerializeField] private WeaponManager _weaponManager;
|
||||
|
||||
private PlayerStats _stats;
|
||||
private PlayerMovement _movement;
|
||||
private WeaponHitBoxInstance _currentHitBoxInstance;
|
||||
|
||||
/// <summary>下劈 HitBox 命中确认事件(供 DownAttackState 订阅 pogo 弹跳逻辑)。</summary>
|
||||
@@ -20,6 +21,7 @@ namespace BaseGames.Player
|
||||
private void Awake()
|
||||
{
|
||||
_stats = GetComponentInParent<PlayerStats>();
|
||||
_movement = GetComponentInParent<PlayerMovement>();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
@@ -52,30 +54,17 @@ namespace BaseGames.Player
|
||||
|
||||
private void HandleDownHitConfirmed(DamageInfo info) => OnDownHitConfirmed?.Invoke(info);
|
||||
|
||||
// ── 连击段伤害来源切换 ────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// 根据当前连招段切换 HitBox 的 DamageSource(由 AttackState 在每段开始时调用)。
|
||||
/// </summary>
|
||||
public void SetComboSegmentSource(int comboIndex)
|
||||
{
|
||||
WeaponSO w = _weaponManager?.ActiveWeapon;
|
||||
if (w == null) return;
|
||||
|
||||
DamageSourceSO src = comboIndex switch
|
||||
{
|
||||
0 => w.attack1Source,
|
||||
1 => w.attack2Source,
|
||||
2 => w.attack3Source,
|
||||
_ => w.attack1Source,
|
||||
};
|
||||
_weaponManager.ActiveHitBoxInstance?.SetDamageSource(AttackDirection.Ground, src);
|
||||
}
|
||||
|
||||
// ── HitBox 激活(由 State / AnimationEvent 调用)─────────────────────
|
||||
public void EnableWeaponHitBox(AttackDirection dir)
|
||||
/// <summary>
|
||||
/// 激活 HitBox。
|
||||
/// hitBoxId 非空时按 Id 精确激活 Prefab 中对应子节点;空 = 方向默认。
|
||||
/// source 为 null 时回退到 WeaponSO.GetSourceByDir(dir)(方向第 0 段)。
|
||||
/// </summary>
|
||||
public void EnableWeaponHitBox(AttackDirection dir,
|
||||
string hitBoxId = "", DamageSourceSO source = null)
|
||||
{
|
||||
var source = _weaponManager?.ActiveWeapon?.GetSourceByDir(dir);
|
||||
_weaponManager?.ActiveHitBoxInstance?.Activate(dir, source, transform);
|
||||
source ??= _weaponManager?.ActiveWeapon?.GetSourceByDir(dir);
|
||||
_weaponManager?.ActiveHitBoxInstance?.Activate(dir, source, transform, hitBoxId);
|
||||
}
|
||||
|
||||
public void DisableWeaponHitBox(AttackDirection dir)
|
||||
@@ -89,6 +78,12 @@ namespace BaseGames.Player
|
||||
{
|
||||
int gain = _weaponManager?.ActiveWeapon?.soulPowerGain ?? 10;
|
||||
_stats?.AddSoulPower(gain);
|
||||
|
||||
// 攻击命中反嵈:向攻击反方向施加微小后退冲量,增强打击感
|
||||
if (_movement?.Rb != null && info.KnockbackDirection.x != 0f)
|
||||
_movement.Rb.AddForce(
|
||||
new UnityEngine.Vector2(UnityEngine.Mathf.Sign(-info.KnockbackDirection.x) * 2f, 0f),
|
||||
UnityEngine.ForceMode2D.Impulse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,20 @@ namespace BaseGames.Player
|
||||
private SurfaceType _currentSurface = SurfaceType.Ground;
|
||||
private readonly Collider2D[] _groundBuffer = new Collider2D[4];
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// ── 运行时调试(Inspector 中可见)───────────────────────────────
|
||||
[Header("\u2500\u2500 \u8fd0\u884c\u65f6\u8c03\u8bd5 \u2500\u2500")]
|
||||
[SerializeField] private string _dbg_Position;
|
||||
[SerializeField] private float _dbg_VelocityX;
|
||||
[SerializeField] private float _dbg_VelocityY;
|
||||
[SerializeField] private bool _dbg_IsGrounded;
|
||||
[SerializeField] private bool _dbg_HasCoyoteTime;
|
||||
[SerializeField] private bool _dbg_IsWallLeft;
|
||||
[SerializeField] private bool _dbg_IsWallRight;
|
||||
[SerializeField] private bool _dbg_CancelWindowOpen;
|
||||
[SerializeField] private int _dbg_FacingDirection;
|
||||
#endif
|
||||
|
||||
public bool IsGrounded => _isGrounded;
|
||||
public bool HasCoyoteTime => _coyoteTimer > 0f;
|
||||
public bool IsWallLeft => _isWallLeft;
|
||||
@@ -54,9 +68,12 @@ namespace BaseGames.Player
|
||||
private void Awake()
|
||||
{
|
||||
Debug.Assert(_config != null, "[PlayerMovement] _config 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
|
||||
Debug.Assert(_groundCheck != null, "[PlayerMovement] GroundCheck 子节点未赋值,地面检测将无法工作,请在 Inspector 中指定 GroundCheck Transform。", this);
|
||||
_rb = GetComponent<Rigidbody2D>();
|
||||
// 关闭位置插值:若开启插值,渲染位置会在速度清零后仍追赶 1~2 渲染帧,产生视觉滑行。
|
||||
_rb.interpolation = RigidbodyInterpolation2D.None;
|
||||
// 开启位置插值:在物理帧(50Hz)与渲染帧(60Hz+)之间平滑视觉位置,消除跳帧抖动。
|
||||
// SpritePixelSnapper(LateUpdate +1000)在插值结果基础上吸附到像素网格,
|
||||
// 与 CameraPixelSnapper 同格对齐,消除亚像素模糊;停止时 ≤2 帧像素追赶不可感知。
|
||||
_rb.interpolation = RigidbodyInterpolation2D.Interpolate;
|
||||
if (_spriteRenderer == null)
|
||||
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||
}
|
||||
@@ -77,6 +94,18 @@ namespace BaseGames.Player
|
||||
_coyoteTimer = _config.CoyoteTime;
|
||||
else
|
||||
_coyoteTimer = Mathf.Max(0f, _coyoteTimer - Time.fixedDeltaTime);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
_dbg_VelocityX = _rb.velocity.x;
|
||||
_dbg_VelocityY = _rb.velocity.y;
|
||||
_dbg_IsGrounded = _isGrounded;
|
||||
_dbg_HasCoyoteTime = _coyoteTimer > 0f;
|
||||
_dbg_IsWallLeft = _isWallLeft;
|
||||
_dbg_IsWallRight = _isWallRight;
|
||||
_dbg_CancelWindowOpen = _cancelWindowOpen;
|
||||
_dbg_FacingDirection = _facingDirection;
|
||||
_dbg_Position = $"({transform.position.x:F1}, {transform.position.y:F1})";
|
||||
#endif
|
||||
}
|
||||
|
||||
// ── 移动 ──────────────────────────────────────────────────────────────
|
||||
@@ -169,8 +198,8 @@ namespace BaseGames.Player
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 壁滑:将垂直速度限制为 -WallSlideSpeed(向下缓慢滑动)。
|
||||
/// WallSlideState.OnStateFixedUpdate 每帧调用。
|
||||
/// 壁滑:将垂直速度限制为 -WallSlideSpeed(受限抓墙时向下缓慢滑动)。
|
||||
/// WallSlideState.OnStateFixedUpdate 在受限模式下每帧调用。
|
||||
/// </summary>
|
||||
public void ApplyWallSlide()
|
||||
{
|
||||
@@ -180,29 +209,42 @@ namespace BaseGames.Player
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 蹬墙跳:对墙方向施加相反水平力 + 向上力。
|
||||
/// wallDir = +1 (右墙) 或 -1 (左墙),跳跃方向与之相反。
|
||||
/// 背墙跳(Jump Away):远离墙壁斜上方弹出。
|
||||
/// wallDir = +1 (右墙) 或 -1 (左墙),水平方向与之相反。
|
||||
/// </summary>
|
||||
public void WallJump(int wallDir)
|
||||
public void WallJumpAway(int wallDir)
|
||||
{
|
||||
float forceX = -wallDir * _config.WallJumpForceX;
|
||||
float forceY = _config.WallJumpForceY;
|
||||
_rb.velocity = new Vector2(forceX, forceY);
|
||||
_rb.velocity = new Vector2(-wallDir * _config.WallJumpAwayForceX, _config.WallJumpAwayForceY);
|
||||
_coyoteTimer = 0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对墙跳(Jump Toward):沿墙壁方向偏向正上方弹出,水平分量较小。
|
||||
/// wallDir = +1 (右墙) 或 -1 (左墙),水平方向与之相同。
|
||||
/// </summary>
|
||||
public void WallJumpToward(int wallDir)
|
||||
{
|
||||
_rb.velocity = new Vector2(wallDir * _config.WallJumpTowardForceX, _config.WallJumpTowardForceY);
|
||||
_coyoteTimer = 0f;
|
||||
}
|
||||
|
||||
/// <summary>将垂直速度归零(抓墙悬挂时每帧调用,防止下滑)。</summary>
|
||||
public void ZeroVerticalVelocity()
|
||||
{
|
||||
if (_rb.velocity.y < 0f)
|
||||
_rb.velocity = new Vector2(_rb.velocity.x, 0f);
|
||||
}
|
||||
|
||||
/// <summary>单向平台穿透(输入下行 + 跳跃键时触发)。</summary>
|
||||
public void DropThroughPlatform() { }
|
||||
|
||||
// ── Physics 检测 ──────────────────────────────────────────────────────
|
||||
private void CheckGrounded()
|
||||
{
|
||||
bool wasGrounded = _isGrounded;
|
||||
Vector2 origin = _groundCheck != null
|
||||
? (Vector2)_groundCheck.position
|
||||
: (Vector2)transform.position + Vector2.down * 0.5f;
|
||||
if (_groundCheck == null) return;
|
||||
|
||||
_isGrounded = Physics2D.OverlapBoxNonAlloc(origin, _groundCheckSize, 0f, _groundBuffer, _groundLayer) > 0;
|
||||
bool wasGrounded = _isGrounded;
|
||||
_isGrounded = Physics2D.OverlapBoxNonAlloc(_groundCheck.position, _groundCheckSize, 0f, _groundBuffer, _groundLayer) > 0;
|
||||
|
||||
if (_isGrounded && !wasGrounded)
|
||||
_coyoteTimer = _config.CoyoteTime;
|
||||
@@ -234,15 +276,24 @@ namespace BaseGames.Player
|
||||
Vector3 arrowEnd = center + new Vector3(_facingDirection * 0.55f, 0f, 0f);
|
||||
DrawArrow2D(center, arrowEnd, new Color(1f, 0.85f, 0.1f, 0.95f));
|
||||
|
||||
// ── 3. 站立检测框(落地亮绿 / 未落地淡绿)──────────────────────
|
||||
Vector2 gOrigin = _groundCheck != null
|
||||
? (Vector2)_groundCheck.position
|
||||
: (Vector2)transform.position + Vector2.down * 0.5f;
|
||||
// ── 3. 站立检测框(落地亮绿 / 未落地淡绿;未赋值时显示红色警告框)──
|
||||
if (_groundCheck == null)
|
||||
{
|
||||
Gizmos.color = new Color(1f, 0.1f, 0.1f, 0.9f);
|
||||
Gizmos.DrawWireSphere(transform.position, 0.25f);
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.Handles.color = new Color(1f, 0.1f, 0.1f, 1f);
|
||||
UnityEditor.Handles.Label(transform.position + Vector3.up * 0.6f, "GroundCheck 未赋值!");
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
bool grounded = Application.isPlaying && _isGrounded;
|
||||
Gizmos.color = grounded
|
||||
? new Color(0.1f, 1f, 0.3f, 0.9f)
|
||||
: new Color(0.4f, 0.85f, 0.4f, 0.35f);
|
||||
BaseGames.Combat.HitBox.DrawWireRect2D(gOrigin, _groundCheckSize);
|
||||
BaseGames.Combat.HitBox.DrawWireRect2D(_groundCheck.position, _groundCheckSize);
|
||||
}
|
||||
|
||||
// ── 4. 靠墙检测射线(碰墙橙色 / 未碰淡黄,带端点圆点)─────────
|
||||
if (_config == null) return;
|
||||
|
||||
@@ -23,12 +23,21 @@ namespace BaseGames.Player
|
||||
public float FallGravityMult = 3.5f;
|
||||
[Tooltip("最大下落速度(终端速度)。推荐 22。")]
|
||||
public float MaxFallSpeed = 22f;
|
||||
[Tooltip("松开跳跃键时速度保留比例(变高跳)。推荐 0.45,越小跳跃越低。")]
|
||||
[Tooltip("松开跳跃键时速度保留比例(变高跳)。推荐 0.35,越小跳跃越低。")]
|
||||
[Range(0f, 1f)]
|
||||
public float JumpCutMultiplier = 0.45f;
|
||||
public float JumpCutMultiplier = 0.35f;
|
||||
|
||||
[Header("二段跳")]
|
||||
[Tooltip("二段跳初速度。设为与 JumpForce 相同可获得等高二段跳。")]
|
||||
[Header("跳跃 — 顶点悬停")]
|
||||
[Tooltip("顶点悬停触发阈值(单位/秒)。当 |垂直速度| 低于此值时,重力缩减为 ApexGravityMultiplier 倍,\n产生\"滞空感\"。推荐 3。调高 → 悬停段更长;调低 → 悬停段更短乃至消失。")]
|
||||
public float ApexThreshold = 3f;
|
||||
[Tooltip("顶点区间内重力缩减比例(乘以 DefaultGravityScale)。推荐 0.3。\n0 = 完全无重力悬停;1 = 无悬停效果(等同于关闭此功能)。")]
|
||||
[Range(0f, 1f)]
|
||||
public float ApexGravityMultiplier = 0.3f;
|
||||
|
||||
[Header("空中跳跃(N 段跳)")]
|
||||
[Tooltip("腾空期间最多可追加的跳跃次数。1 = 二段跳,2 = 三段跳,以此类推。\n需同时在 PlayerStats 中解锁 DoubleJump 能力,否则此值无效。")]
|
||||
public int MaxAirJumps = 1;
|
||||
[Tooltip("空中追加跳跃的初速度。所有段数共用同一数值;设为与 JumpForce 相同可获得等高多段跳。")]
|
||||
public float DoubleJumpForce = 19f;
|
||||
|
||||
[Header("冲刺")]
|
||||
@@ -38,8 +47,6 @@ namespace BaseGames.Player
|
||||
public float DashDuration = 0.35f;
|
||||
[Tooltip("冲刺冷却时长(秒)。推荐 0.6s,落地后才可再次冲刺。")]
|
||||
public float DashCooldown = 0.6f;
|
||||
[Tooltip("每次腾空可使用的最大空中冲刺次数。通常设为 1(单次空中冲刺)。")]
|
||||
public int MaxAerialDashes = 1;
|
||||
|
||||
[Header("冲刺无敌帧(窗口 < 冲刺时长,且有独立 CD)")]
|
||||
[Tooltip("冲刺无敌窗口时长(秒)。仅为冲刺前段;窗口结束后即使仍在冲刺中也可受伤被打断(推荐 0.20s)。")]
|
||||
@@ -47,17 +54,28 @@ namespace BaseGames.Player
|
||||
[Tooltip("无敌的独立冷却(秒)。CD 内再次冲刺不会获得无敌帧,防止连冲变相持续无敌(推荐 0.9s)。")]
|
||||
public float DashInvincibilityCooldown = 0.9f;
|
||||
|
||||
[Header("蹬墙 / 壁滑")]
|
||||
[Header("抓墙 / 壁滑")]
|
||||
[Tooltip("受限抓墙时(高于 wallGrabY)的下滑速度(单位/秒)。推荐 2。")]
|
||||
public float WallSlideSpeed = 2f;
|
||||
public float WallJumpForceX = 12f;
|
||||
public float WallJumpForceY = 16f;
|
||||
public float WallRayLength = 0.55f;
|
||||
public float WallRayOffsetY = 0.2f;
|
||||
public float WallGrabMaxHeightGain = 0.5f;
|
||||
public float WallGrabReleaseDelay = 0.08f;
|
||||
public float WallJumpBackForceX = 14f;
|
||||
public float WallJumpAwayForceX = 10f;
|
||||
[Tooltip("抓墙高度容差:当前 Y 不超过 wallGrabY + 此值时视为未抬升,防止浮点抖动误判。")]
|
||||
public float WallGrabHeightTolerance = 0.05f;
|
||||
|
||||
[Header("蹬墙跳 — 背墙跳(Jump Away,远离墙壁斜上方)")]
|
||||
[Tooltip("背墙跳水平速度(远离墙壁方向)。推荐 14。")]
|
||||
public float WallJumpAwayForceX = 14f;
|
||||
[Tooltip("背墙跳垂直速度。推荐 18。")]
|
||||
public float WallJumpAwayForceY = 18f;
|
||||
|
||||
[Header("蹬墙跳 — 对墙跳(Jump Toward,沿墙壁斜上方)")]
|
||||
[Tooltip("对墙跳水平速度(朝向墙壁方向,较小)。推荐 6。")]
|
||||
public float WallJumpTowardForceX = 6f;
|
||||
[Tooltip("对墙跳垂直速度(偏向正上方)。推荐 18。")]
|
||||
public float WallJumpTowardForceY = 18f;
|
||||
|
||||
[Header("蹬墙跳 — 公共")]
|
||||
[Tooltip("蹬墙跳后水平输入锁定时长(秒)。防止玩家立即向原墙壁方向输入取消起跳。推荐 0.15。")]
|
||||
public float WallJumpInputLockDuration = 0.15f;
|
||||
|
||||
[Header("重力")]
|
||||
|
||||
@@ -46,6 +46,19 @@ namespace BaseGames.Player
|
||||
private bool _isGodMode;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// ── 运行时调试(Inspector 中可见)───────────────────────────────
|
||||
[Header("\u2500\u2500 \u8fd0\u884c\u65f6\u8c03\u8bd5 \u2500\u2500")]
|
||||
[SerializeField] private string _dbg_HP;
|
||||
[SerializeField] private string _dbg_Soul;
|
||||
[SerializeField] private string _dbg_Spirit;
|
||||
[SerializeField] private string _dbg_Spring;
|
||||
[SerializeField] private bool _dbg_IsInvincible;
|
||||
[SerializeField] private float _dbg_InvincibleTimer;
|
||||
[SerializeField] private bool _dbg_GodMode;
|
||||
[SerializeField] private string _dbg_Abilities;
|
||||
#endif
|
||||
|
||||
// ── 护符属性修改器 ─────────────────────────────────────────────────────────
|
||||
private readonly Dictionary<StatType, float> _flatModifiers = new();
|
||||
private readonly Dictionary<StatType, float> _percentModifiers = new();
|
||||
@@ -68,6 +81,7 @@ namespace BaseGames.Player
|
||||
MaxSpringCharges = _config.MaxSpringCharges;
|
||||
CurrentSpringCharges = MaxSpringCharges;
|
||||
CurrentLingZhu = _config.InitialLingZhu;
|
||||
_unlockedAbilities = _config.InitialAbilities;
|
||||
}
|
||||
|
||||
private void OnEnable() => _onDifficultyChanged?.Subscribe(HandleDifficultyChanged).AddTo(_subs);
|
||||
@@ -101,6 +115,17 @@ namespace BaseGames.Player
|
||||
AddSpiritPower(_config.SpiritRegenRate);
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
_dbg_HP = $"{CurrentHP} / {MaxHP}";
|
||||
_dbg_Soul = $"{CurrentSoulPower} / {MaxSoulPower}";
|
||||
_dbg_Spirit = $"{CurrentSpiritPower} / {MaxSpiritPower}";
|
||||
_dbg_Spring = $"{CurrentSpringCharges} / {MaxSpringCharges}";
|
||||
_dbg_IsInvincible = IsInvincible;
|
||||
_dbg_InvincibleTimer = _invincibleTimer;
|
||||
_dbg_GodMode = _isGodMode;
|
||||
_dbg_Abilities = _unlockedAbilities == AbilityType.None ? "\u65e0" : _unlockedAbilities.ToString();
|
||||
#endif
|
||||
}
|
||||
// ── 护符修改器 API ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -25,5 +25,12 @@ namespace BaseGames.Player
|
||||
|
||||
[Header("初始货币")]
|
||||
public int InitialLingZhu = 0;
|
||||
|
||||
[Header("初始已解锁能力")]
|
||||
[Tooltip("角色出生时默认持有的能力([Flags] 位掩码)。\n\n" +
|
||||
"Dash:地面与空中冲刺统一控制,勾选后即可使用 DashState。\n\n" +
|
||||
"DoubleJump:追加次数由 PlayerMovementConfigSO.MaxAirJumps 决定。\n" +
|
||||
"落地或 Pogo 命中后次数自动重置。")]
|
||||
public AbilityType InitialAbilities = AbilityType.None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,24 +22,74 @@ namespace BaseGames.Player
|
||||
/// <summary>触碰到的墙壁方向:+1 = 右墙,-1 = 左墙,0 = 无墙。</summary>
|
||||
public int WallDirection { get; private set; }
|
||||
|
||||
// 每侧"任意一根射线命中 OR 物理接触点命中"的结果,用于防止下落时卡在矮墙边角
|
||||
private bool _anyRightContact;
|
||||
private bool _anyLeftContact;
|
||||
|
||||
// 物理接触点缓冲区(避免每帧 GC)
|
||||
private Rigidbody2D _rb;
|
||||
private static readonly ContactPoint2D[] _contactBuffer = new ContactPoint2D[8];
|
||||
|
||||
/// <summary>
|
||||
/// 指定方向上是否存在墙壁接触(射线命中 OR 物理接触点命中,任一为 true)。
|
||||
/// 用于在 FallState / JumpState 中防止角色卡在矮墙边角:<br/>
|
||||
/// • 射线检测覆盖"检测点略高于墙顶、仅一根射线命中"的情况;<br/>
|
||||
/// • 物理接触点覆盖"两根射线均高于墙顶,但碰撞体底角已卡在墙顶角"的极端情况。
|
||||
/// </summary>
|
||||
public bool HasPartialContact(int direction) =>
|
||||
direction > 0 ? _anyRightContact : _anyLeftContact;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Debug.Assert(_config != null, "[PlayerWallDetector] _config 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
|
||||
_rb = GetComponent<Rigidbody2D>();
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
bool rightWall = CheckSide(Vector2.right);
|
||||
bool leftWall = CheckSide(Vector2.left);
|
||||
bool rightWall = CheckSide(Vector2.right, out bool anyRightRay);
|
||||
bool leftWall = CheckSide(Vector2.left, out bool anyLeftRay);
|
||||
|
||||
// 物理接触点兜底:两根射线都在墙顶以上时,仍可通过接触点检测到卡角
|
||||
bool physRight = CheckPhysicalContact(1);
|
||||
bool physLeft = CheckPhysicalContact(-1);
|
||||
|
||||
_anyRightContact = anyRightRay || physRight;
|
||||
_anyLeftContact = anyLeftRay || physLeft;
|
||||
|
||||
IsTouchingWall = rightWall || leftWall;
|
||||
WallDirection = rightWall ? 1 : (leftWall ? -1 : 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 每侧发两根射线(TopRay + BottomRay),两根均命中才返回 true。
|
||||
/// 通过物理接触点判断指定方向是否有墙壁(法线 X 分量超过 0.5 的水平接触)。
|
||||
/// direction = +1 检查右侧(接触法线指向左,normal.x < -0.5),
|
||||
/// direction = -1 检查左侧(接触法线指向右,normal.x > +0.5)。
|
||||
/// </summary>
|
||||
private bool CheckSide(Vector2 dir)
|
||||
private bool CheckPhysicalContact(int direction)
|
||||
{
|
||||
if (_rb == null) return false;
|
||||
LayerMask mask = _wallLayer != 0 ? _wallLayer : LayerMask.GetMask("Wall", "Ground");
|
||||
var filter = new ContactFilter2D();
|
||||
filter.SetLayerMask(mask);
|
||||
filter.useTriggers = false;
|
||||
|
||||
int count = _rb.GetContacts(filter, _contactBuffer);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
float nx = _contactBuffer[i].normal.x;
|
||||
// 右侧墙接触:法线指向左(nx < -0.5);左侧墙接触:法线指向右(nx > +0.5)
|
||||
if (direction > 0 && nx < -0.5f) return true;
|
||||
if (direction < 0 && nx > 0.5f) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 每侧发两根射线(TopRay + BottomRay),两根均命中才返回 true。
|
||||
/// <paramref name="anyContact"/> 在任意一根命中时为 true(用于防卡角判断)。
|
||||
/// </summary>
|
||||
private bool CheckSide(Vector2 dir, out bool anyContact)
|
||||
{
|
||||
Vector2 center = transform.position;
|
||||
float len = _config.WallRayLength;
|
||||
@@ -48,6 +98,7 @@ namespace BaseGames.Player
|
||||
|
||||
bool top = Physics2D.Raycast(center + Vector2.up * oy, dir, len, layer);
|
||||
bool bot = Physics2D.Raycast(center + Vector2.down * oy, dir, len, layer);
|
||||
anyContact = top || bot;
|
||||
return top && bot;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,43 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 治愈弹簧系统(架构 05_PlayerModule §7)。
|
||||
/// PlayerStats 中已预留 CurrentSpringCharges / MaxSpringCharges / SpringKillPoints 字段。
|
||||
/// TODO: 实现弹簧充能逻辑:
|
||||
/// - 击杀敌人时调用 PlayerStats.AddSpringKillPoint() 积累充能
|
||||
/// - 按下治愈键时消耗充能槽并恢复玩家 HP
|
||||
/// - 充能满格时触发特殊强化状态(视设计而定)
|
||||
/// 灵泉充能系统(架构 05_PlayerModule §7)。
|
||||
/// 订阅 EVT_EnemyDied 事件,每次击杀调用 PlayerStats.AddKillPoints();
|
||||
/// 积分达到阈值时由 PlayerStats 内部自动增加 SpringCharge 并重置积分。
|
||||
/// 使用灵泉(UseSpring)由 PlayerController 处理输入、SpringState 执行动画与消耗。
|
||||
/// </summary>
|
||||
public class SpringSystem : MonoBehaviour { }
|
||||
public class SpringSystem : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private PlayerStats _stats;
|
||||
[SerializeField] private StringEventChannelSO _onEnemyDied; // EVT_EnemyDied
|
||||
|
||||
private EventSubscription _sub;
|
||||
private bool _subscribed;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_onEnemyDied != null)
|
||||
{
|
||||
_sub = _onEnemyDied.Subscribe(OnEnemyDied);
|
||||
_subscribed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_subscribed)
|
||||
{
|
||||
_sub.Dispose();
|
||||
_subscribed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnemyDied(string _enemyId)
|
||||
{
|
||||
_stats?.AddKillPoints(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 空中冲刺状态(架构 05_PlayerModule §12)。
|
||||
/// 与地面 DashState 独立,消耗 MaxAerialDashes 次数;
|
||||
/// 冲刺方向在进入时锁定为当前朝向(进入时锁定朝向,冲刺期间不可通过输入改变方向)。
|
||||
/// </summary>
|
||||
public class AerialDashState : PlayerStateBase
|
||||
{
|
||||
private float _timer;
|
||||
private int _aerialDashesLeft;
|
||||
private int _facingDir;
|
||||
|
||||
public bool HasAerialDash => _aerialDashesLeft > 0;
|
||||
|
||||
// ── IsInvincible 不再在状态层硬编码,与 DashState 保持一致:
|
||||
// 实际无敌用 Stats.BeginInvincibility(DashInvincibilityDuration) 面题。
|
||||
// PlayerController.TakeDamage 已将 Stats.IsInvincible 纳入硬直判断。
|
||||
|
||||
public AerialDashState(PlayerController owner) : base(owner)
|
||||
{
|
||||
_aerialDashesLeft = 1;
|
||||
}
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
_aerialDashesLeft = Mathf.Max(0, _aerialDashesLeft - 1);
|
||||
_facingDir = Owner.FacingDirection;
|
||||
_timer = Cfg.DashDuration;
|
||||
|
||||
// 无敌帧:与地面冲刺共享同一无敌 CD(DashState._invincibilityCooldownTimer)
|
||||
// 条件 1:已解锁 InvincibleDash
|
||||
// 条件 2:共享无敌冷却已就绪
|
||||
var dashState = Owner.GetState<DashState>();
|
||||
if (Stats != null && Stats.HasAbility(AbilityType.InvincibleDash)
|
||||
&& dashState != null && dashState.CanGrantInvincibility)
|
||||
{
|
||||
Stats.BeginInvincibility(Cfg.DashInvincibilityDuration);
|
||||
dashState.ResetInvincibilityCooldown(Cfg.DashInvincibilityCooldown);
|
||||
}
|
||||
|
||||
// 关闭重力,施加冲刺速度(方向锁定为进入时朝向,不受输入影响)
|
||||
Move?.SetGravityScale(0f);
|
||||
Move?.Dash(new Vector2(_facingDir, 0f), Cfg.DashSpeed);
|
||||
|
||||
// 播放冲刺动画(复用地面冲刺动画)
|
||||
if (AnimCfg?.Dash != null) Anim?.Play(AnimCfg.Dash);
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
_timer -= Time.deltaTime;
|
||||
if (_timer <= 0f)
|
||||
{
|
||||
Move?.SetGravityScale(Cfg.DefaultGravityScale);
|
||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
||||
return;
|
||||
}
|
||||
|
||||
// 撞墙立即终止冲刺(碰到实体墙立即中止,避免压墙卡住)
|
||||
var wd = Owner.WallDetector;
|
||||
if (wd != null && wd.IsTouchingWall && wd.WallDirection == _facingDir)
|
||||
{
|
||||
Move?.SetGravityScale(Cfg.DefaultGravityScale);
|
||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
{
|
||||
Move?.SetGravityScale(Cfg.DefaultGravityScale);
|
||||
}
|
||||
|
||||
public override void OnStateFixedUpdate()
|
||||
{
|
||||
// 冲刺期间保持锁定方向速度(与 DashState 一致,使用 _facingDir)
|
||||
if (_timer > 0f)
|
||||
Move?.Dash(new Vector2(_facingDir, 0f), Cfg.DashSpeed);
|
||||
}
|
||||
|
||||
/// <summary>着地时重置空中冲刺次数(由 PlayerController 在着地时调用)。</summary>
|
||||
public void ResetAerialDashes()
|
||||
{
|
||||
_aerialDashesLeft = Cfg.MaxAerialDashes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,38 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 空中攻击状态(架构 05_PlayerModule §2)。
|
||||
/// 由 FallState / JumpState 接收攻击输入后转入;
|
||||
/// Animancer 帧事件驱动 HitBoxAir 激活;结束后回到 FallState。
|
||||
/// HitBox 时间由 ComboStepConfig.hitBoxEnter/Exit 驱动;结束后按 recoveryTime 延迟取消窗口。
|
||||
/// </summary>
|
||||
public class AirAttackState : PlayerStateBase
|
||||
{
|
||||
private float _recoveryEndTime;
|
||||
|
||||
public AirAttackState(PlayerController owner) : base(owner) { }
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
Owner.Combat?.SetComboSegmentSource(0);
|
||||
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Air);
|
||||
var step = Owner.Weapon?.ActiveWeapon?.GetDirStep(AttackDirection.Air);
|
||||
float spd = Stats?.AnimatorSpeedMultiplier ?? 1f;
|
||||
|
||||
var clip = Owner.Weapon?.ActiveWeapon?.airAttackClip;
|
||||
if (clip != null && clip.Clip != null)
|
||||
if (step?.clip?.Clip != null)
|
||||
{
|
||||
var state = Anim.Play(clip);
|
||||
state.Events(this).OnEnd = OnClipEnd;
|
||||
}
|
||||
else if (AnimCfg?.AirAttack != null)
|
||||
{
|
||||
var state = Anim.Play(AnimCfg.AirAttack);
|
||||
state.Events(this).OnEnd = OnClipEnd;
|
||||
var animState = Anim.Play(step.Value.clip);
|
||||
animState.Speed *= spd;
|
||||
|
||||
var events = animState.Events(this);
|
||||
events.OnEnd = OnClipEnd;
|
||||
|
||||
var s = step.Value;
|
||||
events.Add(s.hitBoxEnter, () => Owner.Combat?.EnableWeaponHitBox(AttackDirection.Air, s.hitBoxId));
|
||||
events.Add(s.hitBoxExit, () => Owner.Combat?.DisableAllWeaponHitBoxes());
|
||||
}
|
||||
else
|
||||
{
|
||||
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Air);
|
||||
OnClipEnd();
|
||||
}
|
||||
}
|
||||
@@ -36,10 +40,24 @@ namespace BaseGames.Player.States
|
||||
public override void OnStateExit()
|
||||
{
|
||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
||||
Move?.SetCancelWindowOpen(false);
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
if (Time.time >= _recoveryEndTime)
|
||||
Move.SetCancelWindowOpen(true);
|
||||
}
|
||||
|
||||
private void OnClipEnd()
|
||||
{
|
||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
||||
var step = Owner.Weapon?.ActiveWeapon?.GetDirStep(AttackDirection.Air);
|
||||
float spd = Stats?.AnimatorSpeedMultiplier ?? 1f;
|
||||
float recovery = (step?.recoveryTime ?? 0.05f) / spd;
|
||||
_recoveryEndTime = Time.time + recovery;
|
||||
Move.SetCancelWindowOpen(false);
|
||||
|
||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +1,193 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 地面攻击状态(3 段连击)。
|
||||
/// 由 PlayerController 实例化,AttackEvent 触发切换。
|
||||
/// 通过 Animancer 帧事件驱动 HitBox 激活/关闭。
|
||||
/// 地面攻击状态(任意段连击)。
|
||||
/// 攻速倍率(Stats.AnimatorSpeedMultiplier)缩放 clip 播放速度及 recoveryTime/comboTimeout。
|
||||
/// 状态机流程:
|
||||
/// 播放动画 → [comboInputOpen] 接受输入 → [cancelWindowOpen] 允许跳跃/冲刺 →
|
||||
/// 动画结束 → 硬直(recoveryTime) → 连击等待(comboTimeout) → 无输入则回 Idle
|
||||
/// </summary>
|
||||
public class AttackState : PlayerStateBase
|
||||
{
|
||||
private int _comboIndex;
|
||||
private bool _comboInputPending; // 连击窗口内已收到攻击输入
|
||||
private bool _comboWindowOpen; // 当前是否接受连击输入
|
||||
|
||||
// 动画结束后的两阶段计时
|
||||
private bool _waitingAfterAnim; // 是否在动画结束后等待阶段
|
||||
private float _recoveryEndTime; // 硬直结束时刻
|
||||
private float _comboTimeoutEnd; // 连击等待结束时刻
|
||||
|
||||
public AttackState(PlayerController owner) : base(owner) { }
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
_comboIndex = 0;
|
||||
PlayAttackClip();
|
||||
_comboInputPending = false;
|
||||
_comboWindowOpen = false;
|
||||
_waitingAfterAnim = false;
|
||||
Input.AttackEvent += OnAttackInput;
|
||||
PlayAttackClip();
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
{
|
||||
Input.AttackEvent -= OnAttackInput;
|
||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
||||
Move?.SetCancelWindowOpen(false);
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
if (!_waitingAfterAnim)
|
||||
{
|
||||
// ── 动画播放中:只处理取消窗口(跳跃/冲刺打断)──────────────
|
||||
if (!Move.CancelWindowOpen) return;
|
||||
TryConsumeCancelInput();
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 动画结束后等待阶段 ─────────────────────────────────────────
|
||||
float now = Time.time;
|
||||
|
||||
// 硬直结束后开放取消窗口
|
||||
if (!Move.CancelWindowOpen && now >= _recoveryEndTime)
|
||||
Move.SetCancelWindowOpen(true);
|
||||
|
||||
// 有缓存的连击输入 → 立即推进
|
||||
if (_comboInputPending)
|
||||
{
|
||||
AdvanceCombo();
|
||||
return;
|
||||
}
|
||||
|
||||
// 连击超时 → 返回 Idle
|
||||
if (now >= _comboTimeoutEnd)
|
||||
{
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
return;
|
||||
}
|
||||
|
||||
// 恢复期结束后仍可跳跃/冲刺取消
|
||||
if (Move.CancelWindowOpen)
|
||||
TryConsumeCancelInput();
|
||||
}
|
||||
|
||||
public override void OnStateUpdate() { }
|
||||
public override void OnStateFixedUpdate() { }
|
||||
|
||||
// ── 内部 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void PlayAttackClip()
|
||||
{
|
||||
// ⚠️ 字段名 GroundAttacks(非 AttackChainClips)
|
||||
var clip = AnimCfg.GroundAttacks[_comboIndex];
|
||||
var state = Anim.Play(clip);
|
||||
var events = state.Events(this);
|
||||
_waitingAfterAnim = false;
|
||||
_comboWindowOpen = false;
|
||||
Move.SetCancelWindowOpen(false);
|
||||
|
||||
var weapon = Owner.Weapon?.ActiveWeapon;
|
||||
if (weapon == null)
|
||||
{
|
||||
UnityEngine.Debug.LogWarning("[AttackState] 未找到 ActiveWeapon,请检查 WeaponManager 配置。");
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
return;
|
||||
}
|
||||
|
||||
var step = weapon.GetGroundStep(_comboIndex);
|
||||
if (step.clip == null || step.clip.Clip == null)
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"[AttackState] 连击段 {_comboIndex} 动画未配置,请检查 {weapon.weaponId}。");
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
return;
|
||||
}
|
||||
|
||||
float spd = Stats?.AnimatorSpeedMultiplier ?? 1f;
|
||||
var animState = Anim.Play(step.clip);
|
||||
animState.Speed *= spd; // 在 ClipTransition 自身速度基础上叠加攻速
|
||||
|
||||
var events = animState.Events(this);
|
||||
events.OnEnd = OnClipEnd;
|
||||
|
||||
// HitBox 由 Animancer 归一化时间事件驱动(时间点配置于 PlayerAnimationConfigSO)
|
||||
var timings = AnimCfg?.GroundAttackTimings;
|
||||
float enterTime = timings != null && _comboIndex < timings.Length
|
||||
? timings[_comboIndex].HitBoxEnter : 0.3f;
|
||||
float exitTime = timings != null && _comboIndex < timings.Length
|
||||
? timings[_comboIndex].HitBoxExit : 0.6f;
|
||||
events.Add(enterTime, () => Owner.Combat?.EnableWeaponHitBox(AttackDirection.Ground));
|
||||
events.Add(exitTime, () => Owner.Combat?.DisableAllWeaponHitBoxes());
|
||||
// HitBox 时间窗口(capture step by value for closure safety)
|
||||
var capturedStep = step;
|
||||
events.Add(capturedStep.hitBoxEnter, () =>
|
||||
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Ground,
|
||||
capturedStep.hitBoxId, capturedStep.damageSource));
|
||||
events.Add(capturedStep.hitBoxExit, () => Owner.Combat?.DisableAllWeaponHitBoxes());
|
||||
// 连击输入窗口
|
||||
if (capturedStep.comboInputOpen > 0f)
|
||||
events.Add(capturedStep.comboInputOpen, () => _comboWindowOpen = true);
|
||||
else
|
||||
_comboWindowOpen = true; // 0 = 立即开放
|
||||
|
||||
if (capturedStep.comboInputClose > 0f)
|
||||
events.Add(capturedStep.comboInputClose, () => _comboWindowOpen = false);
|
||||
|
||||
// 取消窗口(跳跃/冲刺)
|
||||
if (capturedStep.cancelWindowOpen > 0f)
|
||||
events.Add(capturedStep.cancelWindowOpen, () => Move.SetCancelWindowOpen(true));
|
||||
}
|
||||
|
||||
private void OnClipEnd()
|
||||
{
|
||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
_comboWindowOpen = false;
|
||||
Move.SetCancelWindowOpen(false);
|
||||
|
||||
// 如果已有缓存输入,直接推进(零延迟连击)
|
||||
if (_comboInputPending)
|
||||
{
|
||||
AdvanceCombo();
|
||||
return;
|
||||
}
|
||||
|
||||
// 进入动画后等待阶段
|
||||
var step = Owner.Weapon?.ActiveWeapon?.GetGroundStep(_comboIndex) ?? default;
|
||||
float spd = Stats?.AnimatorSpeedMultiplier ?? 1f;
|
||||
float now = Time.time;
|
||||
|
||||
_waitingAfterAnim = true;
|
||||
_recoveryEndTime = now + step.recoveryTime / spd;
|
||||
_comboTimeoutEnd = _recoveryEndTime + step.comboTimeout / spd;
|
||||
}
|
||||
|
||||
private void AdvanceCombo()
|
||||
{
|
||||
int maxCombo = Owner.Weapon?.ActiveWeapon?.GroundComboCount ?? 1;
|
||||
if (_comboIndex < maxCombo - 1)
|
||||
{
|
||||
_comboIndex++;
|
||||
_comboInputPending = false;
|
||||
PlayAttackClip();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 已是最后一段,忽略多余输入,等待超时
|
||||
_comboInputPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void TryConsumeCancelInput()
|
||||
{
|
||||
if (Buffer.ConsumeJump())
|
||||
{
|
||||
Owner.TransitionTo(Owner.GetState<JumpState>());
|
||||
return;
|
||||
}
|
||||
var ds = Owner.GetState<DashState>();
|
||||
if (ds != null && ds.CanDash
|
||||
&& Stats != null && Stats.HasAbility(AbilityType.Dash)
|
||||
&& Buffer.ConsumeDash())
|
||||
{
|
||||
Owner.TransitionTo(ds);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAttackInput()
|
||||
{
|
||||
// 连击段数从配置 SO 动态读取,无须修改代码即可支持任意段数连击
|
||||
int maxCombo = AnimCfg?.GroundAttacks?.Length ?? 1;
|
||||
if (_comboIndex < maxCombo - 1)
|
||||
{
|
||||
_comboIndex++;
|
||||
PlayAttackClip();
|
||||
}
|
||||
if (_comboWindowOpen || _waitingAfterAnim)
|
||||
_comboInputPending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,15 @@ namespace BaseGames.Player.States
|
||||
private float _invincibilityCooldownTimer;
|
||||
private int _facingDir;
|
||||
|
||||
/// <summary>本次离地后是否已消耗过一次空中冲刺。落地或下劈命中(Pogo)时重置。</summary>
|
||||
private bool _airDashUsed;
|
||||
|
||||
public bool CanDash => _cooldownTimer <= 0f;
|
||||
/// <summary>空中冲刺可用条件:冷却就绪 且 本次离地内尚未冲刺过。</summary>
|
||||
public bool CanAirDash => _cooldownTimer <= 0f && !_airDashUsed;
|
||||
|
||||
/// <summary>重置空中冲刺次数(落地或 Pogo 时由 IdleState/RunState/DownAttackState 调用)。</summary>
|
||||
public void ResetAirDash() => _airDashUsed = false;
|
||||
|
||||
/// <summary>
|
||||
/// 无敌帧是否已冷却,即本次冲刺可以获得无敌。
|
||||
@@ -44,11 +52,20 @@ namespace BaseGames.Player.States
|
||||
_facingDir = Owner.FacingDirection;
|
||||
_timer = Cfg.DashDuration;
|
||||
|
||||
// 空中冲刺:记录本次离地已使用冲刺(地面冲刺不消耗,仅空中限制一次)
|
||||
if (!Move.IsGrounded)
|
||||
_airDashUsed = true;
|
||||
|
||||
// 无敌帧:
|
||||
// 条件 1:已解锁 InvincibleDash
|
||||
// 条件 2:无敌冷却已就绪(防止 spam 冲刺连序无敌)
|
||||
// 窗口时长 = DashInvincibilityDuration < DashDuration,冲刺后段无保护
|
||||
if (Stats != null && Stats.HasAbility(AbilityType.InvincibleDash) && CanGrantInvincibility)
|
||||
// 在设置冷却计时器前捕获,供后续动画选择使用
|
||||
bool isInvincibleDash = Stats != null
|
||||
&& Stats.HasAbility(AbilityType.InvincibleDash)
|
||||
&& CanGrantInvincibility;
|
||||
|
||||
if (isInvincibleDash)
|
||||
{
|
||||
Stats.BeginInvincibility(Cfg.DashInvincibilityDuration);
|
||||
_invincibilityCooldownTimer = Cfg.DashInvincibilityCooldown;
|
||||
@@ -58,8 +75,11 @@ namespace BaseGames.Player.States
|
||||
Move?.SetGravityScale(0f);
|
||||
Move?.Dash(new Vector2(_facingDir, 0f), Cfg.DashSpeed);
|
||||
|
||||
// 播放冲刺动画
|
||||
if (AnimCfg?.Dash != null) Anim?.Play(AnimCfg.Dash);
|
||||
// 播放冲刺动画:无敌冲刺使用专属 Clip,留空时回退到普通冲刺 Clip
|
||||
var dashClip = (isInvincibleDash && AnimCfg?.DashInvincible != null)
|
||||
? AnimCfg.DashInvincible
|
||||
: AnimCfg?.Dash;
|
||||
if (dashClip != null) Anim?.Play(dashClip);
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
@@ -71,10 +91,19 @@ namespace BaseGames.Player.States
|
||||
return;
|
||||
}
|
||||
|
||||
// 撞墙立即终止冲刺(碰到实体墙立即中止,避免压墙卡住)
|
||||
var wd = Owner.WallDetector;
|
||||
if (wd != null && wd.IsTouchingWall && wd.WallDirection == _facingDir)
|
||||
EndDash();
|
||||
// 跳跃可取消冲刺:冲刺期间按跳跃立即中断并起跳。
|
||||
// 空中冲刺时若有剩余空中跳跃次数,消耗一次并使用二段跳力度。
|
||||
if (Buffer.ConsumeJump())
|
||||
{
|
||||
if (!Move.IsGrounded && Owner.AirJumpsLeft > 0)
|
||||
{
|
||||
Owner.UseAirJump();
|
||||
Owner.GetState<JumpState>()?.SetDoubleJump(true);
|
||||
}
|
||||
Owner.TransitionTo(Owner.GetState<JumpState>());
|
||||
return;
|
||||
}
|
||||
// 注:碰墙时不中止冲刺,完成完整冲刺时长(物理阻止位移,但计时继续)
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
@@ -93,6 +122,8 @@ namespace BaseGames.Player.States
|
||||
|
||||
private void EndDash()
|
||||
{
|
||||
// 双轴速度归零,防止冲刺结束时角色带着 DashSpeed 冲出平台边缘后继续向前飞行。
|
||||
Move?.ZeroVelocity();
|
||||
if (Move != null && Move.IsGrounded)
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
else
|
||||
|
||||
@@ -23,21 +23,22 @@ namespace BaseGames.Player.States
|
||||
if (Owner.Combat != null)
|
||||
Owner.Combat.OnDownHitConfirmed += OnDownHitConfirmed;
|
||||
|
||||
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Down);
|
||||
var step = Owner.Weapon?.ActiveWeapon?.GetDirStep(AttackDirection.Down);
|
||||
float spd = Stats?.AnimatorSpeedMultiplier ?? 1f;
|
||||
|
||||
var clip = Owner.Weapon?.ActiveWeapon?.downAttackClip;
|
||||
if (clip != null && clip.Clip != null)
|
||||
if (step?.clip?.Clip != null)
|
||||
{
|
||||
var state = Anim.Play(clip);
|
||||
state.Events(this).OnEnd = OnClipEnd;
|
||||
}
|
||||
else if (AnimCfg?.DownAttack != null)
|
||||
{
|
||||
var state = Anim.Play(AnimCfg.DownAttack);
|
||||
state.Events(this).OnEnd = OnClipEnd;
|
||||
var animState = Anim.Play(step.Value.clip);
|
||||
animState.Speed *= spd;
|
||||
var events = animState.Events(this);
|
||||
events.OnEnd = OnClipEnd;
|
||||
var s = step.Value;
|
||||
events.Add(s.hitBoxEnter, () => Owner.Combat?.EnableWeaponHitBox(AttackDirection.Down, s.hitBoxId));
|
||||
events.Add(step.Value.hitBoxExit, () => Owner.Combat?.DisableAllWeaponHitBoxes());
|
||||
}
|
||||
else
|
||||
{
|
||||
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Down);
|
||||
OnClipEnd();
|
||||
}
|
||||
|
||||
@@ -58,7 +59,9 @@ namespace BaseGames.Player.States
|
||||
{
|
||||
if (_hasHitEnemy) return;
|
||||
_hasHitEnemy = true;
|
||||
// Pogo 弹跳:命中敌人后向上弹起
|
||||
// Pogo 弹跳:命中敌人后向上弹起,同时重置空中能力(等同落地效果)
|
||||
Owner.ResetAirJumps();
|
||||
Owner.GetState<DashState>()?.ResetAirDash();
|
||||
Move.Jump();
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user