Add WeaponFeedback component and AddressableManagerWindow meta file

- Implemented WeaponFeedback class for handling weapon-related feedbacks such as hit effects and attack sounds.
- Added meta file for AddressableManagerWindow to manage addressable assets.
- Included a new jump.data file for profiler data.
This commit is contained in:
2026-05-22 22:03:32 +08:00
parent 3e1f234ddc
commit b7baf7ad6a
44 changed files with 1783 additions and 1927 deletions

View File

@@ -15,7 +15,7 @@ MonoBehaviour:
m_DefaultGroup: 9ce5c865a2d3a0840aabdd8ccb3fd4b1
m_currentHash:
serializedVersion: 2
Hash: 00000000000000000000000000000000
Hash: 3b5a6592fec2f53c65ab132b7f731fb2
m_OptimizeCatalogSize: 0
m_BuildRemoteCatalog: 0
m_BundleLocalCatalog: 0
@@ -88,6 +88,14 @@ MonoBehaviour:
m_LabelTable:
m_LabelNames:
- default
- Preload
- Poolable
- Enemy
- BGM
- SFX
- Charms
- Config
- Weapon
m_SchemaTemplates: []
m_GroupTemplateObjects:
- {fileID: 11400000, guid: f9701da6026b3a54f9b4d6eb144ee443, type: 2}

View File

@@ -134,6 +134,7 @@ MonoBehaviour:
weaponTrailPrefab: {fileID: 0}
trailColor: {r: 1, g: 1, b: 1, a: 1}
soulPowerGain: 10
hitWeight: 1
references:
version: 2
RefIds: []

View File

@@ -12,23 +12,25 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 81da55e0fcf99d34693cbc5a348225c3, type: 3}
m_Name: PLY_PlayerMovementConfig
m_EditorClassIdentifier:
RunSpeed: 8
RunSpeed: 6
AirDragFactor: 1
JumpForce: 24
JumpForce: 20
CoyoteTime: 0.12
FallGravityMult: 2.5
MaxFallSpeed: 28
JumpCutMultiplier: 0.321
ApexThreshold: 3
ApexGravityMultiplier: 0.3
MaxAirJumps: 1
DoubleJumpForce: 19
MaxAirJumps: 5
DoubleJumpForce: 15
DashSpeed: 20
DashDuration: 0.25
DashCooldown: 0.4
DashInvincibilityDuration: 0.2
DashInvincibilityCooldown: 0.9
WallSlideSpeed: 2
DownDashSpeed: 22
DownDashDuration: 0.25
WallSlideSpeed: 3
WallHangSpeed: 1
WallRayLength: 0.37
WallRayOffsetY: 0.2

View File

@@ -1,101 +1,5 @@
%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: 14
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: ATK_Ground_1
_rivalHitBoxMask:
serializedVersion: 2
m_Bits: 134217792
--- !u!1 &1932889250901504761
GameObject:
m_ObjectHideFlags: 0
@@ -123,7 +27,7 @@ Transform:
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_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -161,7 +65,7 @@ BoxCollider2D:
m_IsTrigger: 1
m_UsedByEffector: 0
m_UsedByComposite: 0
m_Offset: {x: -0.027121663, y: -0.15051937}
m_Offset: {x: 0, y: 0}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0, y: 0}
@@ -172,7 +76,7 @@ BoxCollider2D:
adaptiveTiling: 0
m_AutoTiling: 0
serializedVersion: 2
m_Size: {x: 1.189852, y: 0.80103874}
m_Size: {x: 1, y: 0.5}
m_EdgeRadius: 0
--- !u!114 &6478051166999031478
MonoBehaviour:
@@ -188,11 +92,11 @@ MonoBehaviour:
m_EditorClassIdentifier:
_defaultSource: {fileID: 0}
_hitCooldown: 0.1
_id: ATK_Down
_id:
_rivalHitBoxMask:
serializedVersion: 2
m_Bits: 134217792
--- !u!1 &2584603199706918030
m_Bits: 0
--- !u!1 &3989564331693126876
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -200,38 +104,38 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1660186156348129284}
- component: {fileID: 1152578598430080845}
- component: {fileID: 3007294148525084107}
- component: {fileID: 8294071144630811572}
- component: {fileID: 4949779957213724475}
- component: {fileID: 4757677899241504248}
m_Layer: 14
m_Name: HitBox_Ground _2
m_Name: HitBox_Ground
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1660186156348129284
--- !u!4 &8294071144630811572
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2584603199706918030}
m_GameObject: {fileID: 3989564331693126876}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0.798, y: 0, z: 0}
m_LocalPosition: {x: 0, 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
--- !u!61 &4949779957213724475
BoxCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2584603199706918030}
m_GameObject: {fileID: 3989564331693126876}
m_Enabled: 1
m_Density: 1
m_Material: {fileID: 0}
@@ -257,7 +161,7 @@ BoxCollider2D:
m_IsTrigger: 1
m_UsedByEffector: 0
m_UsedByComposite: 0
m_Offset: {x: -0.117884755, y: 0.01309824}
m_Offset: {x: 0, y: 0}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0, y: 0}
@@ -268,15 +172,15 @@ BoxCollider2D:
adaptiveTiling: 0
m_AutoTiling: 0
serializedVersion: 2
m_Size: {x: 0.7642305, y: 1.1956644}
m_Size: {x: 1, y: 0.5}
m_EdgeRadius: 0
--- !u!114 &3007294148525084107
--- !u!114 &4757677899241504248
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2584603199706918030}
m_GameObject: {fileID: 3989564331693126876}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
@@ -284,106 +188,10 @@ MonoBehaviour:
m_EditorClassIdentifier:
_defaultSource: {fileID: 0}
_hitCooldown: 0.1
_id: ATK_Ground_2
_id:
_rivalHitBoxMask:
serializedVersion: 2
m_Bits: 134217792
--- !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: 14
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: ATK_Air_2
_rivalHitBoxMask:
serializedVersion: 2
m_Bits: 134217792
--- !u!1 &4335406389674002762
GameObject:
m_ObjectHideFlags: 0
@@ -411,7 +219,7 @@ Transform:
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_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -449,7 +257,7 @@ BoxCollider2D:
m_IsTrigger: 1
m_UsedByEffector: 0
m_UsedByComposite: 0
m_Offset: {x: 0.072324514, y: 0}
m_Offset: {x: 0, y: 0}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0, y: 0}
@@ -460,7 +268,7 @@ BoxCollider2D:
adaptiveTiling: 0
m_AutoTiling: 0
serializedVersion: 2
m_Size: {x: 1.1599612, y: 1}
m_Size: {x: 0.5, y: 1}
m_EdgeRadius: 0
--- !u!114 &1392799324577637263
MonoBehaviour:
@@ -476,10 +284,10 @@ MonoBehaviour:
m_EditorClassIdentifier:
_defaultSource: {fileID: 0}
_hitCooldown: 0.1
_id: ATK_Up
_id:
_rivalHitBoxMask:
serializedVersion: 2
m_Bits: 134217792
m_Bits: 0
--- !u!1 &4821376343125962025
GameObject:
m_ObjectHideFlags: 0
@@ -510,12 +318,10 @@ Transform:
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 7119158475861943178}
- {fileID: 1660186156348129284}
- {fileID: 8294071144630811572}
- {fileID: 7468586589501741901}
- {fileID: 6088225995420515986}
- {fileID: 4362395311111627733}
- {fileID: 4405470499151834857}
- {fileID: 6913225169405126738}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &3691925044832415471
@@ -530,11 +336,11 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: ec12dacf2519f58429dd3c59da8f93b0, type: 3}
m_Name:
m_EditorClassIdentifier:
_hitBoxGround: {fileID: 4639356286286040131}
_hitBoxGround: {fileID: 4757677899241504248}
_hitBoxUp: {fileID: 1392799324577637263}
_hitBoxDown: {fileID: 6478051166999031478}
_hitBoxAir: {fileID: 9014207169512774676}
--- !u!1 &8582289489283119946
_hitBoxAir: {fileID: 1382006829078153708}
--- !u!1 &6434981771063321190
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -542,38 +348,38 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4362395311111627733}
- component: {fileID: 922051492914393482}
- component: {fileID: 9014207169512774676}
- component: {fileID: 6913225169405126738}
- component: {fileID: 6843760498109474434}
- component: {fileID: 1382006829078153708}
m_Layer: 14
m_Name: HitBox_Air_1
m_Name: HitBox_Air
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &4362395311111627733
--- !u!4 &6913225169405126738
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8582289489283119946}
m_GameObject: {fileID: 6434981771063321190}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0.553, y: 0, z: 0}
m_LocalPosition: {x: 0, 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
--- !u!61 &6843760498109474434
BoxCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8582289489283119946}
m_GameObject: {fileID: 6434981771063321190}
m_Enabled: 1
m_Density: 1
m_Material: {fileID: 0}
@@ -599,7 +405,7 @@ BoxCollider2D:
m_IsTrigger: 1
m_UsedByEffector: 0
m_UsedByComposite: 0
m_Offset: {x: 0.46717286, y: 0}
m_Offset: {x: 0, y: 0}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0, y: 0}
@@ -610,15 +416,15 @@ BoxCollider2D:
adaptiveTiling: 0
m_AutoTiling: 0
serializedVersion: 2
m_Size: {x: 1.4343457, y: 1}
m_Size: {x: 0.5, y: 1}
m_EdgeRadius: 0
--- !u!114 &9014207169512774676
--- !u!114 &1382006829078153708
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8582289489283119946}
m_GameObject: {fileID: 6434981771063321190}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
@@ -626,7 +432,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
_defaultSource: {fileID: 0}
_hitCooldown: 0.1
_id: ATK_Air_1
_id:
_rivalHitBoxMask:
serializedVersion: 2
m_Bits: 134217792
m_Bits: 0

View File

@@ -39,6 +39,8 @@ namespace BaseGames.Camera
[SerializeField] private CinemachineBrain _brain;
private UnityEngine.Camera _camera;
private CinemachineCamera _cachedVCam;
private CinemachineConfiner3D _cachedConfiner;
private void Reset()
{
@@ -77,7 +79,8 @@ namespace BaseGames.Camera
/// <summary>
/// 获取当前活跃 VCam 的 CinemachineConfiner3D 边界盒(世界空间 AABB
/// 用于在像素取整后将相机钳制回限位区域内。
/// 缓存上次查询的 VCam 实例;仅在活跃 VCam 发生切换时重新调用 GetComponent
/// 避免每帧 GetComponent 开销。
/// </summary>
private bool TryGetActiveConfinerBounds(out Bounds bounds)
{
@@ -85,9 +88,14 @@ namespace BaseGames.Camera
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;
// 只在活跃 VCam 切换时刷新缓存
if (!ReferenceEquals(vcam, _cachedVCam))
{
_cachedVCam = vcam;
_cachedConfiner = vcam.GetComponent<CinemachineConfiner3D>();
}
if (_cachedConfiner == null || !_cachedConfiner.IsValid) return false;
bounds = _cachedConfiner.BoundingVolume.bounds;
return true;
}

View File

@@ -114,7 +114,8 @@ namespace BaseGames.Camera
}
// 触发区域进入:更新集合(同一区域去重后重新加入,保证最新优先级)
_activeZones.RemoveAll(e => e.area == area);
for (int i = _activeZones.Count - 1; i >= 0; i--)
if (_activeZones[i].area == area) _activeZones.RemoveAt(i);
_activeZones.Add((area, priority));
// 仅当此区域是当前最优且尚未激活时才切换
@@ -132,7 +133,9 @@ namespace BaseGames.Camera
if (releasedArea == null) return;
bool wasActive = releasedArea == _currentArea;
int removed = _activeZones.RemoveAll(e => e.area == releasedArea);
int removed = 0;
for (int i = _activeZones.Count - 1; i >= 0; i--)
if (_activeZones[i].area == releasedArea) { _activeZones.RemoveAt(i); removed++; }
// 若区域本就不在栈中,且又不是当前激活区,则无需任何操作
if (removed == 0 && !wasActive) return;

View File

@@ -57,9 +57,14 @@ namespace BaseGames.Camera
// ── 静态:跨实例共享触发状态 ──────────────────────────────────────────
// 玩家当前物理上所在的所有触发区域(按进入顺序排列)
private static readonly List<CameraTriggerZone> s_InsideZones = new();
// 场景内所有已启用的触发区域,供 RoomController 等查询(替代 FindObjectsOfType
private static readonly List<CameraTriggerZone> s_AllZones = new();
// 当前已向 ICameraService 发出 SwitchArea 请求的触发区域
private static CameraTriggerZone s_ActiveZone;
/// <summary>场景内当前所有已启用的触发区域(只读)。</summary>
public static IReadOnlyList<CameraTriggerZone> AllZones => s_AllZones;
/// <summary>
/// 在每次进入 Play Mode 前(或禁用 Domain Reload 时的跨会话)重置静态状态,
/// 防止上一次游戏会话残留的区域引用导致触发逻辑错误。
@@ -68,6 +73,7 @@ namespace BaseGames.Camera
private static void ResetStaticState()
{
s_InsideZones.Clear();
s_AllZones.Clear();
s_ActiveZone = null;
}
@@ -77,12 +83,22 @@ namespace BaseGames.Camera
_collider.isTrigger = true;
}
private void OnEnable()
{
if (Application.isPlaying)
s_AllZones.Add(this);
}
private void OnDisable()
{
if (!Application.isPlaying) return;
s_AllZones.Remove(this);
HandlePlayerExit();
}
/// <summary>判断世界坐标点是否在本触发区域多边形内(供 RoomController 等无需 GetComponent 直接查询)。</summary>
public bool ContainsPoint(Vector2 worldPoint) => _collider != null && _collider.OverlapPoint(worldPoint);
/// <summary>
/// 若玩家出生时已在触发区域内OnTriggerEnter2D 不会触发。
/// 延迟一帧(确保 RoomController.Start 先完成基准区域设置)后主动检测。

View File

@@ -142,8 +142,8 @@ namespace BaseGames.Combat
bool isRivalHitBoxLayer = (_rivalHitBoxMask.value & (1 << otherLayer)) != 0;
if (isRivalHitBoxLayer && CanClash)
{
var rivalHitBox = other.GetComponent<HitBox>();
if (rivalHitBox != null && rivalHitBox.IsActive && rivalHitBox.CanClash)
if (other.TryGetComponent<HitBox>(out var rivalHitBox) &&
rivalHitBox.IsActive && rivalHitBox.CanClash)
{
_clashService?.ResolveClash(this, rivalHitBox);
return; // 拼刀,中止伤害流水线
@@ -151,8 +151,7 @@ namespace BaseGames.Combat
}
// ② 命中 HurtBox
var hurtBox = other.GetComponent<HurtBox>();
if (hurtBox != null)
if (other.TryGetComponent<HurtBox>(out var hurtBox))
{
// 用 HitBox 自身碰撞盒中心在 HurtBox 表面上的最近点作为受击位置。
// 对大体积/长条形受击体(如地刺),此点远比 HurtBox 节点中心更准确。
@@ -163,7 +162,8 @@ namespace BaseGames.Combat
}
// ③ 命中 IBreakable机关/障碍物)
other.GetComponent<IBreakable>()?.TryInteract(info);
if (other.TryGetComponent<IBreakable>(out var breakable))
breakable.TryInteract(info);
}
// ── 当前激活期已命中目标集合(防止复合子 Collider 导致同帧多次命中)────────────

View File

@@ -18,6 +18,8 @@ namespace BaseGames.Combat
protected Rigidbody2D _rb;
protected HitBox _hitBox;
protected float _aliveTimer;
// Lifetime 在 Initialize 时缓存,避免 Update 每帧访问 SO 成员并做 null check
private float _lifetime = float.MaxValue;
private PooledObject _pooledObject;
@@ -32,6 +34,7 @@ namespace BaseGames.Combat
public virtual void Initialize(ProjectileConfigSO config, DamageInfo damageInfo, Vector2 direction, int ownerLayer = 0)
{
_config = config;
_lifetime = config.Lifetime;
DamageInfo = damageInfo;
Direction = direction.normalized;
_aliveTimer = 0f;
@@ -75,7 +78,7 @@ namespace BaseGames.Combat
protected virtual void Update()
{
_aliveTimer += Time.deltaTime;
if (_config != null && _aliveTimer >= _config.Lifetime)
if (_aliveTimer >= _lifetime)
ReturnToPool();
}
@@ -91,6 +94,7 @@ namespace BaseGames.Combat
protected virtual void OnDisable()
{
_aliveTimer = 0f;
_lifetime = float.MaxValue; // 归还对象池后重置,防止未初始化时自毁
}
}
}

View File

@@ -19,6 +19,11 @@ namespace BaseGames.Combat
public event System.Action<DamageInfo> OnHitConfirmed;
private Coroutine _returnCoroutine;
// 按 duration 缓存 WaitForSeconds同一技能复用无 GC 分配
private WaitForSeconds _cachedWait;
private float _cachedWaitDuration = float.NaN;
private void Awake()
{
foreach (var hb in _hitBoxes)
@@ -35,7 +40,31 @@ namespace BaseGames.Combat
hb?.Activate(source, attacker);
}
/// <summary>duration 秒后自动销毁此 GameObject。</summary>
/// <summary>
/// duration 秒后归还对象池SetActive false
/// 由 SkillManager 对象池调用;替代旧版 Destroy 流程。
/// </summary>
public void AutoReturnAfter(float duration)
{
if (!Mathf.Approximately(_cachedWaitDuration, duration))
{
_cachedWaitDuration = duration;
_cachedWait = new WaitForSeconds(duration);
}
if (_returnCoroutine != null) StopCoroutine(_returnCoroutine);
_returnCoroutine = StartCoroutine(ReturnCoroutine());
}
private System.Collections.IEnumerator ReturnCoroutine()
{
yield return _cachedWait;
foreach (var hb in _hitBoxes)
hb?.Deactivate();
_returnCoroutine = null;
gameObject.SetActive(false); // 触发对象池回收
}
/// <summary>duration 秒后销毁(非池化路径,保留向后兼容)。</summary>
public void AutoDestroyAfter(float duration)
=> Destroy(gameObject, Mathf.Max(0f, duration));

View File

@@ -11,8 +11,9 @@ namespace BaseGames.Combat.StatusEffects
public override StatusEffectType EffectType => StatusEffectType.Fire;
public override int MaxStacks => 1;
private static readonly StatusEffectType[] s_MutualExclusions = { StatusEffectType.Freeze };
/// <summary>施加燃烧时移除冻结(火冰互斥)。</summary>
public override StatusEffectType[] MutualExclusions => new[] { StatusEffectType.Freeze };
public override StatusEffectType[] MutualExclusions => s_MutualExclusions;
public FireEffect()
{

View File

@@ -27,6 +27,8 @@ namespace BaseGames.Combat.StatusEffects
// ── Shader 渲染MaterialPropertyBlock不修改共享材质─────────
private SpriteRenderer _renderer;
private MaterialPropertyBlock _propBlock;
// 缓存 Shader 属性 ID避免每次调用 SetShaderParam 都做字符串哈希查找
private readonly Dictionary<string, int> _shaderPropIds = new();
// ── DoT 伤害代理(由 StatusEffect.OnTick 通过 Owner 调用)──────────
private IDamageable _damageable;
@@ -135,8 +137,13 @@ namespace BaseGames.Combat.StatusEffects
public void SetShaderParam(string param, float value)
{
if (_renderer == null) return;
if (!_shaderPropIds.TryGetValue(param, out int propId))
{
propId = Shader.PropertyToID(param);
_shaderPropIds[param] = propId;
}
_renderer.GetPropertyBlock(_propBlock);
_propBlock.SetFloat(param, value);
_propBlock.SetFloat(propId, value);
_renderer.SetPropertyBlock(_propBlock);
}

View File

@@ -24,6 +24,16 @@ namespace BaseGames.Core
/// <item><b>Room</b>:极短淡出(<see cref="_roomFadeDuration"/>),无加载画面。</item>
/// <item><b>Scene</b>:完整淡出(<see cref="_sceneFadeDuration"/>),显示加载画面。</item>
/// </list>
/// <para>
/// 完整加载时序(保证场景物体在显示前完成状态初始化):
/// <list type="number">
/// <item>淡出(黑幕遮挡)</item>
/// <item>Addressable 异步加载场景(场景物体 Awake / OnEnable 同步执行)</item>
/// <item>触发 <see cref="_onSceneWorldStateRestored"/>:通知场景物体从 WorldStateRegistry 恢复初始状态</item>
/// <item>等待一帧(保证所有场景物体 Start() 和事件处理器执行完毕)</item>
/// <item>淡入(显示已完成初始化的场景)</item>
/// </list>
/// </para>
/// </summary>
[DefaultExecutionOrder(-900)]
public class SceneService : MonoBehaviour, ISceneService
@@ -35,6 +45,12 @@ namespace BaseGames.Core
[SerializeField] private VoidEventChannelSO _onFadeInRequest;
[SerializeField] private VoidEventChannelSO _onFadeOutRequest;
[Tooltip("场景加载完成、WorldStateRegistry 已就绪后触发。\n" +
"场景内物体应订阅此事件,从 WorldStateRegistry 读取存档状态并应用(替代在 Start() 中读取)。\n" +
"触发后会等待一帧,确保所有处理器执行完毕,再执行淡入显示场景。\n" +
"对应 SOEVT_SceneWorldStateRestored")]
[SerializeField] private VoidEventChannelSO _onSceneWorldStateRestored;
[SerializeField] private SceneLoader _sceneLoader;
[Header("淡出时长")]
@@ -71,6 +87,14 @@ namespace BaseGames.Core
else
Debug.LogError("[SceneService] _sceneLoader 未赋值,场景加载中断。请在 Inspector 中绑定 SceneLoader 组件。");
// 通知WorldStateRegistry 已就绪,场景物体应在此帧内从中读取存档状态并应用初始状态。
// 订阅者WorldStateRegistrySaver、各场景 StateApplier 等)会在同一帧同步执行。
_onSceneWorldStateRestored?.Raise();
// 等待一帧:确保所有场景物体的 Start() 和事件处理器都已执行完毕,
// 场景物体处于正确的初始状态后再揭开黑幕,避免出现一帧状态错误的画面闪烁。
yield return null;
_onFadeInRequest?.Raise();
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: abd3b31b261435e4786f53b937c71742
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,527 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;
// 此文件已被 AddressableManagerWindow 取代。
// 原有功能已整合到统一工具中,请使用:
// BaseGames → Addressables → Addressables Manager总入口
// BaseGames → Addressables → Rule Sync直达规则校验 Tab
namespace BaseGames.Editor
{
/// <summary>
/// Addressable 规则同步窗口。
///
/// 功能:
/// 1. 扫描所有已注册的 Addressable 资产
/// 2. 根据 <see cref="AddressableRules"/> 中的规则计算期望分组与期望标签
/// 3. 对比实际值,显示所有不符合规范的条目(分组错误 / 标签缺失 / 标签多余)
/// 4. 一键自动修复全部问题
/// 5. 导出 CSV 报告供存档或 Code Review
///
/// 菜单BaseGames → Addressables → Rule Sync
/// </summary>
public class AddressableRuleSyncWindow : EditorWindow
{
// ── 内部数据结构 ───────────────────────────────────────────────────────
private enum IssueKind { None, WrongGroup, MissingLabel, ExtraLabel }
private class EntryReport
{
public string Address;
public string AssetPath;
public string CurrentGroup;
public string ExpectedGroup; // null = 规则未覆盖,维持现状
public string[] CurrentLabels;
public string[] ExpectedLabels;
public string[] MissingLabels; // 应有但没有(规则要求),红色错误
public string[] ExtraLabels; // 规则不要求且在 KnownLabels 中(多余规则标签),红色错误
public string[] UnknownLabels; // 规则不要求且不在 KnownLabels 中(自定义标签),黄色警告,不自动删除
public bool GroupOk => ExpectedGroup == null || CurrentGroup == ExpectedGroup;
public bool LabelsOk => MissingLabels.Length == 0 && ExtraLabels.Length == 0;
public bool IsOk => GroupOk && LabelsOk;
public bool HasWarnings => UnknownLabels.Length > 0;
}
// ── 状态 ──────────────────────────────────────────────────────────────
private List<EntryReport> _reports = new();
private Vector2 _scrollPos;
private bool _showOk = false;
private bool _scanned = false;
private string _searchFilter = "";
// ── 样式(惰性初始化)────────────────────────────────────────────────
private GUIStyle _okStyle;
private GUIStyle _warnStyle;
private GUIStyle _errorStyle;
private GUIStyle _boldStyle;
private GUIStyle _rowEven;
private GUIStyle _rowOdd;
private bool _stylesReady;
// ── 颜色 ─────────────────────────────────────────────────────────────
private static readonly Color ColOk = new(0.20f, 0.78f, 0.35f, 1f);
private static readonly Color ColWarn = new(0.95f, 0.75f, 0.10f, 1f);
private static readonly Color ColError = new(0.90f, 0.25f, 0.20f, 1f);
private static readonly Color ColRowEven = new(0.22f, 0.22f, 0.22f, 0.4f);
// ── 菜单入口 ──────────────────────────────────────────────────────────
[MenuItem("BaseGames/Addressables/Rule Sync", priority = 110)]
public static void OpenWindow()
{
var win = GetWindow<AddressableRuleSyncWindow>("Addressable Rule Sync");
win.minSize = new Vector2(1040, 540);
win.Show();
}
// ── GUI ───────────────────────────────────────────────────────────────
private void OnGUI()
{
EnsureStyles();
if (AddressableAssetSettingsDefaultObject.Settings == null)
{
EditorGUILayout.HelpBox(
"Addressable Settings 未初始化。\n" +
"请先执行 Window → Asset Management → Addressables → Groups → Create Addressables Settings。",
MessageType.Error);
return;
}
DrawToolbar();
DrawStats();
DrawTable();
DrawFooter();
}
// ── 工具栏 ────────────────────────────────────────────────────────────
private void DrawToolbar()
{
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
{
if (GUILayout.Button("扫描", EditorStyles.toolbarButton, GUILayout.Width(80)))
Scan();
if (GUILayout.Button("🔄 刷新", EditorStyles.toolbarButton, GUILayout.Width(60)))
Scan();
GUILayout.Space(8);
_showOk = GUILayout.Toggle(_showOk, "显示正常项", EditorStyles.toolbarButton, GUILayout.Width(80));
GUILayout.Space(8);
EditorGUILayout.LabelField("搜索:", GUILayout.Width(42));
_searchFilter = EditorGUILayout.TextField(_searchFilter, EditorStyles.toolbarSearchField,
GUILayout.Width(200));
GUILayout.FlexibleSpace();
GUI.enabled = _scanned && _reports.Any(r => !r.IsOk);
if (GUILayout.Button("✦ 修复所有问题", EditorStyles.toolbarButton, GUILayout.Width(120)))
FixAll();
GUI.enabled = _scanned;
if (GUILayout.Button("导出 CSV", EditorStyles.toolbarButton, GUILayout.Width(80)))
ExportCsv();
GUI.enabled = true;
}
}
// ── 统计行 ────────────────────────────────────────────────────────────
private void DrawStats()
{
if (!_scanned) return;
int total = _reports.Count;
int ok = _reports.Count(r => r.IsOk);
int issues = _reports.Count(r => !r.IsOk);
int warnings = _reports.Count(r => r.IsOk && r.HasWarnings);
int wrongGrp = _reports.Count(r => !r.GroupOk);
int misLabel = _reports.Count(r => r.MissingLabels.Length > 0);
int extLabel = _reports.Count(r => r.ExtraLabels.Length > 0);
int unkLabel = _reports.Count(r => r.UnknownLabels.Length > 0);
EditorGUILayout.Space(2);
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.Label($"共 {total} 条目", EditorStyles.miniLabel);
GUILayout.Space(12);
DrawColoredLabel($"✅ 正常 {ok}", ColOk);
GUILayout.Space(12);
DrawColoredLabel($"❌ 问题 {issues}", issues > 0 ? ColError : ColOk);
GUILayout.Space(8);
DrawColoredLabel($"⚠ 自定义标签 {unkLabel}", unkLabel > 0 ? ColWarn : ColOk);
GUILayout.Space(20);
GUILayout.Label($"分组错误 {wrongGrp} | 标签缺失 {misLabel} | 多余规则标签 {extLabel}",
EditorStyles.miniLabel);
GUILayout.FlexibleSpace();
}
EditorGUILayout.Space(2);
}
// ── 主表格 ────────────────────────────────────────────────────────────
private void DrawTable()
{
// 表头
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
{
GUILayout.Label("Address", _boldStyle, GUILayout.Width(200));
GUILayout.Label("当前分组", _boldStyle, GUILayout.Width(120));
GUILayout.Label("期望分组", _boldStyle, GUILayout.Width(120));
GUILayout.Label("缺失标签", _boldStyle, GUILayout.Width(130));
GUILayout.Label("多余规则标签", _boldStyle, GUILayout.Width(110));
GUILayout.Label("自定义标签", _boldStyle, GUILayout.Width(110));
GUILayout.Label("状态", _boldStyle, GUILayout.Width(80));
}
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, GUILayout.ExpandHeight(true));
if (!_scanned)
{
EditorGUILayout.HelpBox("点击「扫描」按钮开始分析已注册的 Addressable 资产。", MessageType.Info);
}
else
{
var display = _reports
.Where(r => _showOk || !r.IsOk)
.Where(r => string.IsNullOrEmpty(_searchFilter)
|| r.Address.IndexOf(_searchFilter, StringComparison.OrdinalIgnoreCase) >= 0)
.ToList();
if (display.Count == 0)
{
EditorGUILayout.HelpBox(
_showOk ? "没有匹配搜索条件的条目。" : "✅ 所有资产均符合规范!",
MessageType.Info);
}
for (int i = 0; i < display.Count; i++)
DrawRow(display[i], i);
}
EditorGUILayout.EndScrollView();
}
private void DrawRow(EntryReport r, int idx)
{
var bg = idx % 2 == 0 ? _rowEven : GUIStyle.none;
using (new EditorGUILayout.HorizontalScope(bg, GUILayout.Height(20)))
{
// Address点击可 Ping
if (GUILayout.Button(r.Address, EditorStyles.linkLabel, GUILayout.Width(200)))
PingAsset(r.AssetPath);
// 当前分组
var grpColor = r.GroupOk ? ColOk : ColError;
DrawColoredLabel(r.CurrentGroup ?? "—", grpColor, GUILayout.Width(120));
// 期望分组
var expGrpText = r.ExpectedGroup ?? "(规则未覆盖)";
var expGrpColor = r.GroupOk ? ColOk : ColWarn;
DrawColoredLabel(expGrpText, expGrpColor, GUILayout.Width(120));
// 缺失标签(红色,须补齐)
var missingText = r.MissingLabels.Length > 0 ? string.Join(", ", r.MissingLabels) : "—";
DrawColoredLabel(missingText, r.MissingLabels.Length > 0 ? ColError : ColOk, GUILayout.Width(130));
// 多余规则标签(红色,将被 FixEntry 移除)
var extraText = r.ExtraLabels.Length > 0 ? string.Join(", ", r.ExtraLabels) : "—";
DrawColoredLabel(extraText, r.ExtraLabels.Length > 0 ? ColError : ColOk, GUILayout.Width(110));
// 自定义标签(黄色警告,不会被自动删除,建议写入规范)
var unknownText = r.UnknownLabels.Length > 0 ? string.Join(", ", r.UnknownLabels) : "—";
DrawColoredLabel(unknownText, r.UnknownLabels.Length > 0 ? ColWarn : ColOk, GUILayout.Width(110));
// 状态 + 单条修复按钮
if (r.IsOk)
{
var statusColor = r.HasWarnings ? ColWarn : ColOk;
var statusText = r.HasWarnings ? "⚠ 自定义标签" : "✅ 正常";
DrawColoredLabel(statusText, statusColor, GUILayout.Width(80));
}
else
{
DrawColoredLabel("❌ 需修复", ColError, GUILayout.Width(60));
if (GUILayout.Button("修复", EditorStyles.miniButton, GUILayout.Width(40)))
FixEntry(r);
}
}
}
// ── 底栏 ──────────────────────────────────────────────────────────────
private void DrawFooter()
{
EditorGUILayout.Space(4);
EditorGUILayout.HelpBox(
"规则来源Docs/Standards/AddressablesLabelSpec.md §3 分组规则AssetFolderSpec.md §8.1\n" +
"「修复所有问题」仅修改已注册资产的分组/标签,不注册新资产,不删除自定义标签(黄色警告项)。\n" +
"新增资产工作流:① Addressable Batch Tool → ⚡ 全量扫描 _Game/ → 注册所有 ② 返回此窗口 → 扫描 → 修复所有问题",
MessageType.None);
}
// ── 扫描逻辑 ──────────────────────────────────────────────────────────
private void Scan()
{
_reports.Clear();
var settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null) return;
foreach (var group in settings.groups)
{
if (group == null) continue;
foreach (var entry in group.entries)
{
if (entry == null) continue;
var address = entry.address;
var expectedGroup = AddressableRules.GetExpectedGroup(address);
var expectedLbls = AddressableRules.GetExpectedLabels(address);
var currentLbls = entry.labels.ToArray();
var missing = expectedLbls.Except(currentLbls, StringComparer.Ordinal).ToArray();
// 区分两类"多余标签"
// extra = 规则已知标签KnownLabels中规则不要求的 → 红色FixEntry 会移除
// unknown = 不在 KnownLabels 中的自定义标签 → 黄色警告FixEntry 保留,建议写入规范
var notExpected = currentLbls.Except(expectedLbls, StringComparer.Ordinal);
var extra = notExpected.Where(l => AddressableRules.KnownLabels.Contains(l)).ToArray();
var unknown = notExpected.Where(l => !AddressableRules.KnownLabels.Contains(l)).ToArray();
_reports.Add(new EntryReport
{
Address = address,
AssetPath = entry.AssetPath,
CurrentGroup = group.name,
ExpectedGroup = expectedGroup,
CurrentLabels = currentLbls,
ExpectedLabels = expectedLbls,
MissingLabels = missing,
ExtraLabels = extra,
UnknownLabels = unknown,
});
}
}
// 问题项排前面,仅有警告的次之,正常项排最后;同类按 Address 字母序
_reports = _reports
.OrderBy(r => r.IsOk ? (r.HasWarnings ? 1 : 2) : 0)
.ThenBy(r => r.Address, StringComparer.Ordinal)
.ToList();
_scanned = true;
Repaint();
int issues = _reports.Count(r => !r.IsOk);
int warnings = _reports.Count(r => r.IsOk && r.HasWarnings);
Debug.Log($"[AddressableRuleSync] 扫描完成:{_reports.Count} 个条目," +
$"{issues} 个需要修复,{warnings} 个含自定义标签警告。");
}
// ── 修复逻辑 ──────────────────────────────────────────────────────────
private void FixAll()
{
var issues = _reports.Where(r => !r.IsOk).ToList();
if (issues.Count == 0) return;
int moveCount = issues.Count(r => !r.GroupOk);
int addCount = issues.Sum(r => r.MissingLabels.Length);
int removeCount = issues.Sum(r => r.ExtraLabels.Length);
// 干跑预览对话框
bool confirmed = EditorUtility.DisplayDialog(
"确认修复所有问题",
$"将对 {issues.Count} 个条目执行以下操作:\n\n" +
$" • 移动分组:{moveCount} 个\n" +
$" • 添加标签:{addCount} 个\n" +
$" • 移除多余规则标签:{removeCount} 个\n\n" +
"⚠ 自定义标签(黄色警告项)不会被删除。\n" +
"此操作不可撤销,请确认后继续。",
"确认修复", "取消");
if (!confirmed) return;
int fixedCount = 0;
foreach (var r in issues)
{
if (FixEntry(r)) fixedCount++;
}
SaveSettings();
Scan(); // 修复后重新扫描以更新结果
Debug.Log($"[AddressableRuleSync] 修复完成:共处理 {fixedCount} 个条目。");
}
private bool FixEntry(EntryReport r)
{
var settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null) return false;
var entry = FindEntry(settings, r.Address);
if (entry == null)
{
Debug.LogWarning($"[AddressableRuleSync] 找不到条目:{r.Address}");
return false;
}
bool changed = false;
// 修复分组
if (!r.GroupOk && r.ExpectedGroup != null)
{
var targetGroup = GetOrCreateGroup(settings, r.ExpectedGroup);
if (targetGroup != null && entry.parentGroup != targetGroup)
{
settings.MoveEntry(entry, targetGroup, false, false);
r.CurrentGroup = r.ExpectedGroup;
changed = true;
}
}
// 添加缺失标签
foreach (var lbl in r.MissingLabels)
{
EnsureLabelExists(settings, lbl);
entry.SetLabel(lbl, true, true);
changed = true;
}
// 移除多余规则标签ExtraLabels 只包含 KnownLabels 中规则不要求的标签;
// UnknownLabels 是用户自定义标签,刻意保留,不做删除)
foreach (var lbl in r.ExtraLabels)
{
entry.SetLabel(lbl, false, true);
changed = true;
}
return changed;
}
// ── 导出 CSV ──────────────────────────────────────────────────────────
private void ExportCsv()
{
if (_reports.Count == 0) return;
var path = EditorUtility.SaveFilePanel(
"导出 Addressable Rule 报告", "", "AddressableRuleReport.csv", "csv");
if (string.IsNullOrEmpty(path)) return;
var sb = new StringBuilder();
sb.AppendLine("Address,CurrentGroup,ExpectedGroup,GroupOk,MissingLabels,ExtraLabels,Status");
foreach (var r in _reports)
{
var status = r.IsOk ? "OK" : "ISSUE";
sb.AppendLine(
$"\"{r.Address}\"," +
$"\"{r.CurrentGroup}\"," +
$"\"{r.ExpectedGroup ?? "(uncovered)"}\"," +
$"{r.GroupOk}," +
$"\"{string.Join(";", r.MissingLabels)}\"," +
$"\"{string.Join(";", r.ExtraLabels)}\"," +
$"{status}");
}
File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
Debug.Log($"[AddressableRuleSync] CSV 报告已导出:{path}");
}
// ── 辅助方法 ──────────────────────────────────────────────────────────
private static AddressableAssetEntry FindEntry(AddressableAssetSettings settings, string address)
{
foreach (var group in settings.groups)
{
if (group == null) continue;
foreach (var e in group.entries)
if (e != null && e.address == address) return e;
}
return null;
}
private static AddressableAssetGroup GetOrCreateGroup(AddressableAssetSettings settings, string groupName)
{
var existing = settings.groups.FirstOrDefault(g => g != null && g.name == groupName);
if (existing != null) return existing;
var template = settings.GroupTemplateObjects.FirstOrDefault()
as AddressableAssetGroupTemplate;
var newGroup = settings.CreateGroup(groupName, false, false, true,
template != null
? new List<AddressableAssetGroupSchema>(template.SchemaObjects)
: null);
if (newGroup != null)
Debug.Log($"[AddressableRuleSync] 已自动创建分组:{groupName}");
return newGroup ?? settings.DefaultGroup;
}
private static void EnsureLabelExists(AddressableAssetSettings settings, string label)
{
var labels = settings.GetLabels();
if (!labels.Contains(label))
{
settings.AddLabel(label, true);
Debug.Log($"[AddressableRuleSync] 已创建标签:{label}");
}
}
private static void SaveSettings()
{
AssetDatabase.SaveAssets();
AddressableAssetSettingsDefaultObject.Settings?.SetDirty(
AddressableAssetSettings.ModificationEvent.EntryModified, null, true);
}
private static void PingAsset(string assetPath)
{
if (string.IsNullOrEmpty(assetPath)) return;
var obj = AssetDatabase.LoadMainAssetAtPath(assetPath);
if (obj != null) EditorGUIUtility.PingObject(obj);
}
// ── 样式初始化 ────────────────────────────────────────────────────────
private void EnsureStyles()
{
if (_stylesReady) return;
_boldStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 11 };
_okStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = ColOk } };
_warnStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = ColWarn } };
_errorStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = ColError } };
_rowEven = new GUIStyle();
_rowEven.normal.background = MakeTexture(1, 1, ColRowEven);
_stylesReady = true;
}
private void DrawColoredLabel(string text, Color color, params GUILayoutOption[] options)
{
var prev = GUI.color;
GUI.color = color;
GUILayout.Label(text, EditorStyles.miniLabel, options);
GUI.color = prev;
}
private static Texture2D MakeTexture(int width, int height, Color color)
{
var tex = new Texture2D(width, height);
tex.SetPixel(0, 0, color);
tex.Apply();
return tex;
}
}
// 保留空类以避免 .meta 文件孤立。
internal static class AddressableRuleSyncWindowStub { }
}

View File

@@ -81,7 +81,8 @@ namespace BaseGames.Enemies.AI
if (distance > 0.01f)
{
var hit = Physics2D.Raycast(origin, direction.normalized, distance, requester.LOSBlockingMask);
// direction / distance == direction.normalized,避免重复开方
var hit = Physics2D.Raycast(origin, direction / distance, distance, requester.LOSBlockingMask);
// 若无遮挡物hit.collider == null则视线畅通
hasLOS = hit.collider == null;
}

View File

@@ -27,15 +27,18 @@ namespace BaseGames.Enemies
/// <summary>按 SO 配置速度水平移动。dir: +1 右 / -1 左 / 0 停止。</summary>
public void MoveHorizontal(float dir)
{
float speed = _config.WalkSpeed;
_rb.velocity = new Vector2(dir * speed, _rb.velocity.y);
var vel = _rb.velocity;
vel.x = dir * _config.WalkSpeed;
_rb.velocity = vel;
UpdateFacing(dir);
}
/// <summary>显式指定速度BD 追击任务调用)。</summary>
public void MoveWithSpeed(float dir, float speed)
{
_rb.velocity = new Vector2(dir * speed, _rb.velocity.y);
var vel = _rb.velocity;
vel.x = dir * speed;
_rb.velocity = vel;
UpdateFacing(dir);
}
@@ -52,7 +55,9 @@ namespace BaseGames.Enemies
public void StopHorizontal()
{
_rb.velocity = new Vector2(0f, _rb.velocity.y);
var vel = _rb.velocity;
vel.x = 0f;
_rb.velocity = vel;
}
/// <summary>

View File

@@ -23,6 +23,8 @@ namespace BaseGames.Enemies
private readonly HashSet<EnemyBase> _registeredSet = new();
private readonly List<EnemyBase> _registered = new();
private readonly Dictionary<EnemyBase, int> _indexMap = new();
// 排序临时缓冲区:预计算每个敌人到玩家的距离,避免 Sort 比较器内重复 Vector3 运算O(n logn) → O(n)
private readonly List<(EnemyBase enemy, float sqDist)> _sortTemp = new();
// 缓存玩家 Transform
private Transform _playerTransform;
private readonly CompositeDisposable _subs = new();
@@ -72,22 +74,35 @@ namespace BaseGames.Enemies
// ── 内部 ──────────────────────────────────────────────────────────
private void Rebalance()
{
if (_registered.Count == 0) return;
int n = _registered.Count;
if (n == 0) return;
var playerPos = _playerTransform != null ? _playerTransform.position : Vector3.zero;
// 按距离平方升序排序(避免开方,性能更好
_registered.Sort((a, b) =>
// ① 预计算距离O(n) Vector3 运算,而非在比较器内重复执行 O(n logn) 次
_sortTemp.Clear();
for (int i = 0; i < n; i++)
{
if (a == null) return 1;
if (b == null) return -1;
float sqA = (a.transform.position - playerPos).sqrMagnitude;
float sqB = (b.transform.position - playerPos).sqrMagnitude;
return sqA.CompareTo(sqB);
});
var e = _registered[i];
float sqd = e != null
? (e.transform.position - playerPos).sqrMagnitude
: float.MaxValue;
_sortTemp.Add((e, sqd));
}
// ② 对临时列表排序(比较器只做 float 比较,无额外 Vector3 开销)
_sortTemp.Sort(static (a, b) => a.sqDist.CompareTo(b.sqDist));
// ③ 将排序结果写回 _registered同步重建 _indexMap修复排序后索引过期的 bug
for (int i = 0; i < n; i++)
{
var e = _sortTemp[i].enemy;
_registered[i] = e;
if (e != null) _indexMap[e] = i;
}
#if GRAPH_DESIGNER
for (int i = _registered.Count - 1; i >= 0; i--)
for (int i = n - 1; i >= 0; i--)
{
var enemy = _registered[i];
if (enemy == null) { _registered.RemoveAt(i); continue; }

View File

@@ -0,0 +1,64 @@
using UnityEngine;
using MoreMountains.Feedbacks;
namespace BaseGames.Feedback
{
/// <summary>
/// 武器反馈组件,挂载在武器 HitBox Prefab 根节点上。
/// 实现 IFeedbackPlayer专注于武器本地反馈命中粒子、击打音效、破风等
/// 角色级别的全局反馈(震屏、手柄振动)仍由 PlayerFeedback 负责。
/// </summary>
public class WeaponFeedback : MonoBehaviour, IFeedbackPlayer
{
[Header("命中反馈")]
[SerializeField] private MMF_Player _onHitLight;
[SerializeField] private MMF_Player _onHitMedium;
[SerializeField] private MMF_Player _onHitHeavy;
[Header("攻击破风")]
[SerializeField] private MMF_Player _onAttackWhoosh;
[Header("通用命名预设")]
[SerializeField] private NamedFeedbackPreset[] _namedPresets;
[System.Serializable]
private struct NamedFeedbackPreset
{
public string presetId;
public MMF_Player player;
}
// ── IFeedbackPlayer 实现 ──────────────────────────────────────────────
public void PlayHit(HitWeight weight)
{
var player = weight switch
{
HitWeight.Light => _onHitLight,
HitWeight.Heavy => _onHitHeavy,
_ => _onHitMedium,
};
player?.PlayFeedbacks();
}
public void PlayAttackWhoosh() => _onAttackWhoosh?.PlayFeedbacks();
public void TriggerPreset(string presetId)
{
if (_namedPresets == null) return;
foreach (var p in _namedPresets)
if (p.presetId == presetId) { p.player?.PlayFeedbacks(); return; }
}
// ── 武器上不适用的反馈,空实现 ────────────────────────────────────────
public void PlayParrySuccess() { }
public void PlayTakeHit() { }
public void PlayDeath() { }
public void PlayHeal() { }
public void PlayLandImpact() { }
public void PlayJumpLaunch() { }
public void PlayFootstep() { }
public void PlaySFXById(string sfxId) { }
public void PlayFormSwitch(int formIndex){ }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: adb4af2f574f356449634bc130f94592
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -44,6 +44,8 @@ namespace BaseGames.Parry
private ParryPhase _phase = ParryPhase.Inactive;
private float _phaseTimer;
private float _cooldownTimer;
// 缓存 WaitForSecondsRealtime避免每次完美弹反都触发 GC 分配
private WaitForSecondsRealtime _bulletTimeWait;
public ParryPhase CurrentPhase => _phase;
/// <summary>是否处于弹反有效窗口(供外部检测)。</summary>
@@ -74,6 +76,13 @@ namespace BaseGames.Parry
Debug.Assert(_config != null, "[ParrySystem] _config 未赋值,请在 Inspector 中指定 ParryConfigSO。", this);
}
private void Start()
{
// 在 Start 中初始化,确保 _config 已被赋值Awake 之后)
if (_config != null)
_bulletTimeWait = new WaitForSecondsRealtime(_config.BulletTimeDuration);
}
/// <summary>由 PlayerController 在 Awake 中注入 InputReader无需在 Inspector 单独指定。</summary>
public void SetInputReader(InputReaderSO reader)
{
@@ -206,7 +215,7 @@ namespace BaseGames.Parry
private IEnumerator ApplyBulletTime()
{
Time.timeScale = _config.BulletTimeScale;
yield return new WaitForSecondsRealtime(_config.BulletTimeDuration);
yield return _bulletTimeWait;
Time.timeScale = 1f;
}
}

View File

@@ -1,5 +1,6 @@
using UnityEngine;
using BaseGames.Combat;
using BaseGames.Feedback;
namespace BaseGames.Player
{
@@ -14,6 +15,7 @@ namespace BaseGames.Player
private PlayerStats _stats;
private PlayerMovement _movement;
private WeaponHitBoxInstance _currentHitBoxInstance;
private IFeedbackPlayer _feedback;
/// <summary>下劈 HitBox 命中确认事件(供 DownAttackState 订阅 pogo 弹跳逻辑)。</summary>
public event System.Action<DamageInfo> OnDownHitConfirmed;
@@ -22,6 +24,9 @@ namespace BaseGames.Player
{
_stats = GetComponentInParent<PlayerStats>();
_movement = GetComponentInParent<PlayerMovement>();
_feedback = GetComponentInParent<IFeedbackPlayer>()
?? GetComponentInChildren<IFeedbackPlayer>()
?? NullFeedbackPlayer.Instance;
}
private void OnEnable()
@@ -83,6 +88,12 @@ namespace BaseGames.Player
int gain = _weaponManager?.ActiveWeapon?.soulPowerGain ?? 10;
_stats?.AddSoulPower(gain);
// 命中反馈:按伤害量决定力度档位
var weight = info.FinalDamage <= 5 ? HitWeight.Light
: info.FinalDamage <= 15 ? HitWeight.Medium
: HitWeight.Heavy;
_feedback.PlayHit(weight);
// 攻击命中反嵈:向攻击反方向施加微小后退冲量,增强打击感
if (_movement?.Rb != null && info.KnockbackDirection.x != 0f)
_movement.Rb.AddForce(

View File

@@ -46,8 +46,12 @@ namespace BaseGames.Player
private bool _facingLocked; // 为 true 时 UpdateFacing() 不覆盖朝向
private bool _cancelWindowOpen;
private SurfaceType _currentSurface = SurfaceType.Ground;
private readonly Collider2D[] _groundBuffer = new Collider2D[4];
private int _groundHitCount;
private bool _wasGrounded;
// 跳跃/二段跳期间禁用斜坡吸附,防止把起跳判定成斜坡而立即下压
private bool _slopeSnapDisabled;
private readonly Collider2D[] _groundBuffer = new Collider2D[4];
private int _groundHitCount;
private readonly ContactPoint2D[] _slopeContactBuffer = new ContactPoint2D[8];
#if UNITY_EDITOR
// ── 运行时调试Inspector 中可见)───────────────────────────────
@@ -118,17 +122,20 @@ namespace BaseGames.Player
_wallCoyoteTimer = Mathf.Max(0f, _wallCoyoteTimer - Time.fixedDeltaTime);
#if UNITY_EDITOR
_dbg_VelocityX = _rb.velocity.x;
_dbg_VelocityY = _rb.velocity.y;
_dbg_IsGrounded = _isGrounded;
_dbg_OnOneWayPlatform = _onOneWayPlatform;
_dbg_HasCoyoteTime = _coyoteTimer > 0f;
_dbg_HasWallCoyoteTime = _wallCoyoteTimer > 0f;
_dbg_IsWallLeft = _isWallLeft;
_dbg_IsWallRight = _isWallRight;
_dbg_CancelWindowOpen = _cancelWindowOpen;
_dbg_FacingDirection = _facingDirection;
_dbg_Position = $"({transform.position.x:F1}, {transform.position.y:F1})";
// 值类型字段每帧同步(无分配)
_dbg_VelocityX = _rb.velocity.x;
_dbg_VelocityY = _rb.velocity.y;
_dbg_IsGrounded = _isGrounded;
_dbg_OnOneWayPlatform = _onOneWayPlatform;
_dbg_HasCoyoteTime = _coyoteTimer > 0f;
_dbg_HasWallCoyoteTime = _wallCoyoteTimer > 0f;
_dbg_IsWallLeft = _isWallLeft;
_dbg_IsWallRight = _isWallRight;
_dbg_CancelWindowOpen = _cancelWindowOpen;
_dbg_FacingDirection = _facingDirection;
// 字符串格式化限速到 ~10 Hz避免每帧分配
if (Time.frameCount % 6 == 0)
_dbg_Position = $"({transform.position.x:F1}, {transform.position.y:F1})";
#endif
}
@@ -175,6 +182,7 @@ namespace BaseGames.Player
{
_rb.velocity = new Vector2(_rb.velocity.x, _config.JumpForce);
_coyoteTimer = 0f;
_slopeSnapDisabled = true;
}
public void CutJump()
@@ -191,6 +199,7 @@ namespace BaseGames.Player
{
_rb.velocity = new Vector2(_rb.velocity.x, _config.DoubleJumpForce);
_coyoteTimer = 0f;
_slopeSnapDisabled = true;
}
// ── 重力 ──────────────────────────────────────────────────────────────
@@ -379,10 +388,38 @@ namespace BaseGames.Player
{
if (_groundCheck == null) return;
_wasGrounded = _isGrounded;
_groundHitCount = Physics2D.OverlapBoxNonAlloc(
_groundCheck.position, _groundCheckSize, 0f, _groundBuffer, _groundLayer);
_isGrounded = _groundHitCount > 0;
// 斜坡吸附禁用标记:仅在重新落地(从空中→地面)时重置,
// 而非每帧在地面时都重置。
// 这样 Jump() 设置的 _slopeSnapDisabled = true 可以存活到玩家真正离开地面,
// 防止起跳后的首个 FixedUpdate 仍检测到地面时把标记清零,
// 导致紧接着的斜坡吸附把垂直速度归零(即"一直按方向键起跳立即落地"bug
if (_isGrounded && !_wasGrounded)
_slopeSnapDisabled = false;
// 斜坡吸附OverlapBox 是水平矩形,在平地→斜坡转折处可能短暂离地。
// 读取 Rigidbody2D 已有的物理接触点(零额外物理查询开销),
// 接触法线 Y > 0.5 即视为地面接触,保持 IsGrounded 为 true。
if (!_isGrounded && _wasGrounded && !_slopeSnapDisabled
&& Mathf.Abs(_rb.velocity.x) > 0.1f)
{
int contactCount = _rb.GetContacts(_slopeContactBuffer);
for (int i = 0; i < contactCount; i++)
{
if (_slopeContactBuffer[i].normal.y > 0.5f)
{
_isGrounded = true;
_rb.velocity = new Vector2(_rb.velocity.x, 0f);
break;
}
}
}
// 检测是否站在单向平台(含 IDropThrough 组件的碰撞体)
_onOneWayPlatform = false;
for (int i = 0; i < _groundHitCount; i++)

View File

@@ -127,14 +127,19 @@ namespace BaseGames.Player
}
#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();
// 字符串插值限速到 ~10 Hz避免每帧分配GC
if (Time.frameCount % 6 == 0)
{
_dbg_HP = $"{CurrentHP} / {MaxHP}";
_dbg_Soul = $"{CurrentSoulPower} / {MaxSoulPower}";
_dbg_Spirit = $"{CurrentSpiritPower} / {MaxSpiritPower}";
_dbg_Spring = $"{CurrentSpringCharges} / {MaxSpringCharges}";
_dbg_Abilities = _unlockedAbilities == AbilityType.None ? "\u65e0" : _unlockedAbilities.ToString();
}
#endif
}
// ── 护符修改器 API ─────────────────────────────────────────────────────

View File

@@ -29,6 +29,8 @@ namespace BaseGames.Player
// 物理接触点缓冲区(避免每帧 GC
private Rigidbody2D _rb;
private static readonly ContactPoint2D[] _contactBuffer = new ContactPoint2D[8];
// LayerMask 在 Awake 解析一次,避免 FixedUpdate50Hz每帧字符串查找
private LayerMask _resolvedWallMask;
/// <summary>
/// 指定方向上是否存在墙壁接触(射线命中 OR 物理接触点命中,任一为 true
@@ -43,6 +45,7 @@ namespace BaseGames.Player
{
Debug.Assert(_config != null, "[PlayerWallDetector] _config 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
_rb = GetComponent<Rigidbody2D>();
_resolvedWallMask = _wallLayer != 0 ? _wallLayer : LayerMask.GetMask("Wall", "Platform");
}
private void FixedUpdate()
@@ -69,9 +72,8 @@ namespace BaseGames.Player
private bool CheckPhysicalContact(int direction)
{
if (_rb == null) return false;
LayerMask mask = _wallLayer != 0 ? _wallLayer : LayerMask.GetMask("Wall", "Platform");
var filter = new ContactFilter2D();
filter.SetLayerMask(mask);
filter.SetLayerMask(_resolvedWallMask);
filter.useTriggers = false;
int count = _rb.GetContacts(filter, _contactBuffer);
@@ -94,7 +96,7 @@ namespace BaseGames.Player
Vector2 center = transform.position;
float len = _config.WallRayLength;
float oy = _config.WallRayOffsetY;
int layer = _wallLayer != 0 ? (int)_wallLayer : LayerMask.GetMask("Wall", "Platform");
int layer = _resolvedWallMask;
bool top = Physics2D.Raycast(center + Vector2.up * oy, dir, len, layer);
bool bot = Physics2D.Raycast(center + Vector2.down * oy, dir, len, layer);

View File

@@ -82,6 +82,8 @@ namespace BaseGames.Player.States
? AnimCfg.DashInvincible
: AnimCfg?.Dash;
if (dashClip != null) Anim?.Play(dashClip);
Feedback.TriggerPreset("dash");
}
public override void OnStateUpdate()

View File

@@ -19,6 +19,8 @@ namespace BaseGames.Player.States
if (Owner.HurtBox != null)
Owner.HurtBox.SetActive(false);
Feedback.PlayDeath();
// 播放死亡动画
if (AnimCfg?.Dead != null)
Anim?.Play(AnimCfg.Dead);

View File

@@ -28,6 +28,7 @@ namespace BaseGames.Player.States
_timer = Owner.AnimConfig?.HurtDuration ?? 0.4f;
_ended = false;
Stats?.BeginInvincibility();
Feedback.PlayTakeHit();
if (AnimCfg?.Hurt != null)
{

View File

@@ -4,6 +4,7 @@ using Animancer;
using BaseGames.Core.Events;
using BaseGames.Input;
using BaseGames.Combat;
using BaseGames.Feedback;
using BaseGames.Parry;
using BaseGames.Skills;
@@ -35,6 +36,7 @@ namespace BaseGames.Player.States
// ── 战斗组件 ──────────────────────────────────────────────────────────
[Header("战斗")]
[SerializeField] private PlayerFeedback _feedback;
[SerializeField] private PlayerCombat _combat;
[SerializeField] private FormController _formController;
[SerializeField] private WeaponManager _weaponManager;
@@ -57,6 +59,8 @@ namespace BaseGames.Player.States
private InputBuffer _inputBuffer;
private bool _missingDependencyLogged;
private bool _dependenciesReady;
// DashState 在 Update 每帧访问TickCooldown + CanDash提前缓存避免重复 Dictionary 查找
private DashState _dashState;
/// <summary>
/// 当前腾空可用的额外跳跃次数(二段跳)。
/// 由 IdleState/RunState.OnStateEnter 落地时通过 ResetAirJumps() 重置;
@@ -128,6 +132,7 @@ namespace BaseGames.Player.States
public InputReaderSO Input => _inputReader;
public InputBuffer Buffer => _inputBuffer;
public IFeedbackPlayer Feedback => _feedback != null ? (IFeedbackPlayer)_feedback : NullFeedbackPlayer.Instance;
public PlayerCombat Combat => _combat;
public FormController Form => _formController;
public WeaponManager Weapon => _weaponManager;
@@ -272,6 +277,7 @@ namespace BaseGames.Player.States
{
_stats?.AddSoul(info.SoulGained);
_shield?.OnParrySuccess();
Feedback.PlayParrySuccess();
}
/// <summary>灵泉输入:地面且有剩余充能时转入 SpringState 使用一次。</summary>
@@ -301,7 +307,7 @@ namespace BaseGames.Player.States
return;
// 冲刺冷却计时
GetState<DashState>()?.TickCooldown(Time.deltaTime);
_dashState?.TickCooldown(Time.deltaTime);
_currentState?.OnStateUpdate();
@@ -309,7 +315,7 @@ namespace BaseGames.Player.States
_dbg_CurrentState = _currentState?.GetType().Name ?? "None";
_dbg_IsGrounded = _movement != null && _movement.IsGrounded;
_dbg_AirJumpsLeft = _airJumpsLeft;
_dbg_CanDash = GetState<DashState>()?.CanDash ?? false;
_dbg_CanDash = _dashState?.CanDash ?? false;
_dbg_IsInvincible = _stats != null && _stats.IsInvincible;
#endif
}
@@ -359,8 +365,9 @@ namespace BaseGames.Player.States
_states[typeof(HurtState)] = new HurtState(this);
_states[typeof(DeadState)] = new DeadState(this);
_states[typeof(SpringState)] = new SpringState(this);
_states[typeof(ParryState)] = new ParryState(this);
_states[typeof(SwimState)] = new SwimState(this);
_states[typeof(ParryState)] = new ParryState(this);
_states[typeof(SwimState)] = new SwimState(this);
_dashState = (DashState)_states[typeof(DashState)];
}
/// <summary>

View File

@@ -1,4 +1,5 @@
using Animancer;
using BaseGames.Feedback;
using BaseGames.Input;
using BaseGames.Player;
@@ -41,6 +42,7 @@ namespace BaseGames.Player.States
protected InputBuffer Buffer => _owner.Buffer;
protected PlayerMovement Move => _owner.Movement;
protected PlayerStats Stats => _owner.Stats;
protected IFeedbackPlayer Feedback => _owner.Feedback;
protected AnimancerComponent Anim => _owner.Animancer;
protected PlayerMovementConfigSO Cfg => _owner.MovConfig;
protected PlayerAnimationConfigSO AnimCfg => _owner.AnimConfig;

View File

@@ -49,8 +49,9 @@ namespace BaseGames.Player.States
private void OnSpringEnd()
{
// 前摇正常结束 → 执行回血
// 前摇正常结束 → 执行回血 + 反馈
Stats?.ApplySpringHeal();
Feedback.PlayHeal();
Owner.TransitionTo(Owner.GetState<IdleState>());
}
}

View File

@@ -1,5 +1,6 @@
using UnityEngine;
using BaseGames.Combat;
using BaseGames.Feedback;
namespace BaseGames.Player
{
@@ -28,6 +29,7 @@ namespace BaseGames.Player
private HitBox[] _allHitBoxes;
private AttackDirection _activeDir;
private IFeedbackPlayer _feedback;
/// <summary>下劈命中确认事件(供 DownAttackState Pogo 逻辑)。</summary>
public event System.Action<DamageInfo> OnDownHitConfirmed;
@@ -40,10 +42,16 @@ namespace BaseGames.Player
_allHitBoxes = GetComponentsInChildren<HitBox>(true);
foreach (var hb in _allHitBoxes)
hb.OnHitConfirmed += OnAnyHitConfirmed;
_feedback = GetComponentInChildren<IFeedbackPlayer>() ?? NullFeedbackPlayer.Instance;
}
private void OnAnyHitConfirmed(DamageInfo info)
{
var weight = info.FinalDamage <= 5 ? HitWeight.Light
: info.FinalDamage <= 15 ? HitWeight.Medium
: HitWeight.Heavy;
_feedback.PlayHit(weight);
OnHitConfirmed?.Invoke(info);
if (_activeDir == AttackDirection.Down)
OnDownHitConfirmed?.Invoke(info);
@@ -59,6 +67,7 @@ namespace BaseGames.Player
string hitBoxId = "")
{
_activeDir = dir;
_feedback.PlayAttackWhoosh();
var hitBox = string.IsNullOrEmpty(hitBoxId)
? GetHitBox(dir)
: (GetHitBoxById(hitBoxId) ?? GetHitBox(dir));

View File

@@ -59,6 +59,9 @@ namespace BaseGames.Player
[Min(0)]
public int soulPowerGain = 10;
[Tooltip("命中敌人时的打击力度反馈档位(影响摄像机震屏和控制器振动强度)。")]
public HitWeight hitWeight = HitWeight.Medium;
// ── 查询 API ──────────────────────────────────────────────────────────
/// <summary>取指定方向、指定段的完整配置,越界自动取最后一个。</summary>

View File

@@ -12,6 +12,7 @@
"BaseGames.Player",
"BaseGames.Input",
"BaseGames.Combat",
"BaseGames.Feedback",
"Kybernetik.Animancer"
],
"autoReferenced": true,

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using BaseGames.Player;
using BaseGames.Input;
using BaseGames.Combat;
using BaseGames.Feedback;
namespace BaseGames.Skills
{
@@ -41,13 +42,23 @@ namespace BaseGames.Skills
private FormSkillSO _soulSkill;
private FormSkillSO _spirit1;
private FormSkillSO _spirit2;
private IFeedbackPlayer _feedback;
// 冷却字典FormSkillSO → 剩余冷却秒数UpdateSkillSet 时重建
private readonly Dictionary<FormSkillSO, float> _cooldowns = new(3);
// 无分配 Update 遍历用的快照数组
private FormSkillSO[] _activeSkills = System.Array.Empty<FormSkillSO>();
// 技能 HitBox 对象池prefab → 已创建的实例列表,通过 activeSelf 判断是否可复用
private readonly Dictionary<GameObject, List<SkillHitBoxInstance>> _hitBoxPools = new();
// ── 生命周期 ──────────────────────────────────────────────────────────
private void Awake()
{
_feedback = GetComponentInChildren<IFeedbackPlayer>()
?? GetComponentInParent<IFeedbackPlayer>()
?? NullFeedbackPlayer.Instance;
}
private void OnEnable()
{
if (_input != null)
@@ -147,6 +158,9 @@ namespace BaseGames.Skills
_cooldowns[skill] = p.effectiveCooldown;
// 施放反馈
_feedback.TriggerPreset("skill_cast");
// 播放动画(优先修改器动画,回退技能默认动画)
var clip = p.effectiveAnimation.Clip != null
? p.effectiveAnimation
@@ -158,14 +172,40 @@ namespace BaseGames.Skills
if (skill.SkillHitBoxPrefab != null)
{
var socket = _skillSocket != null ? _skillSocket : transform;
var go = Object.Instantiate(skill.SkillHitBoxPrefab, socket.position,
socket.rotation, socket);
var inst = go.GetComponent<SkillHitBoxInstance>();
var inst = GetOrCreateHitBox(skill.SkillHitBoxPrefab, socket);
inst?.Activate(skill.damageSource, transform);
inst?.AutoDestroyAfter(skill.castLockDuration > 0f ? skill.castLockDuration : 0.5f);
inst?.AutoReturnAfter(skill.castLockDuration > 0f ? skill.castLockDuration : 0.5f);
}
}
/// <summary>
/// 从对象池获取或新建 SkillHitBoxInstance。
/// 扫描该 prefab 已创建的实例列表,找到首个未激活的复用;
/// 无可用实例时 Instantiate并追加到列表供下次复用。
/// </summary>
private SkillHitBoxInstance GetOrCreateHitBox(GameObject prefab, Transform socket)
{
if (!_hitBoxPools.TryGetValue(prefab, out var list))
_hitBoxPools[prefab] = list = new List<SkillHitBoxInstance>(2);
for (int i = 0; i < list.Count; i++)
{
var pooled = list[i];
if (pooled != null && !pooled.gameObject.activeSelf)
{
pooled.transform.SetParent(socket);
pooled.transform.SetPositionAndRotation(socket.position, socket.rotation);
pooled.gameObject.SetActive(true);
return pooled;
}
}
var go = Object.Instantiate(prefab, socket.position, socket.rotation, socket);
var inst = go.GetComponent<SkillHitBoxInstance>();
if (inst != null) list.Add(inst);
return inst;
}
// ── 属性查询 ─────────────────────────────────────────────────────────
public FormSkillSO SoulSkill => _soulSkill;
public FormSkillSO Spirit1 => _spirit1;

View File

@@ -12,6 +12,7 @@
"BaseGames.Input",
"BaseGames.Player",
"BaseGames.Combat",
"BaseGames.Feedback",
"BaseGames.Skills",
"Kybernetik.Animancer"
],

View File

@@ -1,6 +1,7 @@
using UnityEngine;
using BaseGames.Player;
using BaseGames.Input;
using BaseGames.Feedback;
namespace BaseGames.Spells
{
@@ -24,9 +25,17 @@ namespace BaseGames.Spells
// 当前装备的法术(单槽;如需多槽可扩展为数组)
private SpellSO _equippedSpell;
private float _cooldownRemaining;
private IFeedbackPlayer _feedback;
// ── 生命周期 ──────────────────────────────────────────────────────────
private void Awake()
{
_feedback = GetComponentInChildren<IFeedbackPlayer>()
?? GetComponentInParent<IFeedbackPlayer>()
?? NullFeedbackPlayer.Instance;
}
private void OnEnable()
{
if (_input != null)
@@ -86,6 +95,9 @@ namespace BaseGames.Spells
_cooldownRemaining = _equippedSpell.cooldown;
// 施放反馈
_feedback.TriggerPreset("spell_cast");
ExecuteSpellEffect(_equippedSpell);
}

View File

@@ -27,11 +27,12 @@ namespace BaseGames.UI
private RectTransform _rectTransform;
private Coroutine _animCoroutine;
// 每次 Show() 解析一次,协程期间(< 1s复用避免每帧走 FindObjectByTag
private Camera _cachedCamera;
private void Awake()
{
_rectTransform = (RectTransform)transform;
// 不在 Awake 缓存 Camera.main避免 Boss 过场切换主摄像机后引用过期
}
/// <summary>
@@ -42,6 +43,12 @@ namespace BaseGames.UI
{
if (_animCoroutine != null) StopCoroutine(_animCoroutine);
// 每次 Show 解析一次摄像机:动画时长 < 1s期间不会切换主摄像机
// 若 Boss 过场后再次 Show会自动获取新的主摄像机。
_cachedCamera = (_parentCanvas != null && _parentCanvas.renderMode == RenderMode.ScreenSpaceCamera)
? _parentCanvas.worldCamera
: UnityEngine.Camera.main;
_text.text = damage.ToString();
_text.color = GetColorForType(type);
@@ -51,9 +58,7 @@ namespace BaseGames.UI
private void SetAnchoredPosition(Vector2 worldPosition)
{
var cam = (_parentCanvas != null && _parentCanvas.renderMode == RenderMode.ScreenSpaceCamera)
? _parentCanvas.worldCamera
: UnityEngine.Camera.main;
var cam = _cachedCamera;
var screenPoint = cam != null
? (Vector2)cam.WorldToScreenPoint(worldPosition)
@@ -122,7 +127,7 @@ namespace BaseGames.UI
[Header("预制体(对象池 key = AddressKeys.PrefabUIFloatingDmgText")]
[SerializeField] private GameObject _floatingDmgPrefab; // FallbackInspector 直接拖入
private readonly Queue<FloatingDamageText> _pool = new();
private readonly List<FloatingDamageText> _pool = new();
private readonly CompositeDisposable _subs = new();
private void OnEnable() => _onDamageDealt?.Subscribe(OnDamageDealt).AddTo(_subs);
@@ -138,25 +143,22 @@ namespace BaseGames.UI
private FloatingDamageText GetOrCreate()
{
// 从池中找到已停用的实例
while (_pool.Count > 0)
// 线性扫描全部已创建实例,找首个未激活的复用
for (int i = 0; i < _pool.Count; i++)
{
var pooled = _pool.Dequeue();
if (pooled == null) continue;
if (!pooled.gameObject.activeSelf)
var pooled = _pool[i];
if (pooled != null && !pooled.gameObject.activeSelf)
{
pooled.gameObject.SetActive(true);
return pooled;
}
_pool.Enqueue(pooled); // 仍在使用,放回
break;
}
// 没有可用实例则实例化
// 没有可用实例则实例化新的,加入列表供下次复用
if (_floatingDmgPrefab == null) return null;
var go = Instantiate(_floatingDmgPrefab, transform);
var comp = go.GetComponent<FloatingDamageText>();
if (comp != null) _pool.Enqueue(comp);
if (comp != null) _pool.Add(comp);
return comp;
}
}

View File

@@ -83,7 +83,11 @@ namespace BaseGames.UI
_maxHP = max;
// 重建阶段标记(每次 BossHPMax 改变时清空并按已存阈值重建,此处简化为清空)
if (_phaseMarkersRoot != null)
foreach (Transform t in _phaseMarkersRoot) Destroy(t.gameObject);
{
// 逆序删除:避免正序枚举 Transform 子节点同时销毁时的迭代器失效
for (int i = _phaseMarkersRoot.childCount - 1; i >= 0; i--)
Destroy(_phaseMarkersRoot.GetChild(i).gameObject);
}
}
// ── 动画协程 ──────────────────────────────────────────────────────────

View File

@@ -42,6 +42,7 @@ namespace BaseGames.UI.HUD
private readonly List<GameObject> _hpCells = new();
private readonly List<GameObject> _springIcons = new();
private readonly CompositeDisposable _subs = new();
private int _lastLingZhu = int.MinValue;
private void OnEnable()
{
@@ -91,7 +92,9 @@ namespace BaseGames.UI.HUD
private void UpdateLingZhu(int val)
{
if (_lingZhuText != null) _lingZhuText.text = val.ToString();
if (_lingZhuText == null || val == _lastLingZhu) return;
_lastLingZhu = val;
_lingZhuText.text = val.ToString();
}
private void RebuildSpringIcons(int charges)

View File

@@ -46,19 +46,17 @@ namespace BaseGames.World
{
Vector2 playerPos = player.transform.position;
#if UNITY_6000_0_OR_NEWER
var zones = Object.FindObjectsByType<CameraTriggerZone>(FindObjectsSortMode.None);
#else
var zones = Object.FindObjectsOfType<CameraTriggerZone>();
#endif
// 使用 CameraTriggerZone 静态注册表,避免 FindObjectsOfType 全场景扫描
var zones = CameraTriggerZone.AllZones;
// 选取优先级最高的匹配区域(避免多区域重叠时选错基线)
CameraArea bestArea = null;
int bestPriority = int.MinValue;
foreach (var zone in zones)
{
var poly = zone.GetComponent<PolygonCollider2D>();
if (poly != null && poly.OverlapPoint(playerPos))
if (zone == null) continue;
if (zone.ContainsPoint(playerPos))
{
var area = zone.GetComponentInParent<CameraArea>();
if (area != null && zone.Priority > bestPriority)