181404010226 před 4 měsíci
rodič
revize
4f3e9b71f5

+ 117 - 11
assets/scripts/CombatSystem/BallController.ts

@@ -71,6 +71,21 @@ export class BallController extends Component {
     private blockFireCooldown: Map<string, number> = new Map();
     private FIRE_COOLDOWN = 0.05;
 
+    // 防围困机制配置
+    @property({
+        tooltip: '防围困检测时间窗口(秒)'
+    })
+    public antiTrapTimeWindow: number = 5.0;
+
+    @property({
+        tooltip: '防围困撞击次数阈值'
+    })
+    public antiTrapHitThreshold: number = 5;
+
+    // 防围困机制状态
+    private ballHitHistory: Map<string, number[]> = new Map(); // 记录每个球的撞击时间历史
+    private ballPhaseThrough: Map<string, number> = new Map(); // 记录每个球的穿透结束时间
+
     // 带尾部特效的子弹容器预制体
     @property({
         type: Prefab,
@@ -627,6 +642,73 @@ export class BallController extends Component {
         if (!ballNode || !otherNode) {
             return;
         }
+
+        // 检查是否为方块碰撞
+        const nodeName = otherNode.name;
+        const nodePath = this.getNodePath(otherNode);
+        const hasWeaponChild = otherNode.getChildByName('Weapon') !== null;
+        const isBlock = 
+            nodeName.includes('Block') || 
+            nodePath.includes('Block') ||
+            hasWeaponChild;
+
+        // 如果是方块碰撞,检查防围困机制
+        if (isBlock) {
+            const ballId = ballNode.uuid;
+            const currentTime = performance.now() / 1000; // 转换为秒
+
+            // 检查是否处于穿透状态
+            const phaseThroughEndTime = this.ballPhaseThrough.get(ballId);
+            if (phaseThroughEndTime && currentTime < phaseThroughEndTime) {
+                // 处于穿透状态,忽略碰撞但仍播放特效
+                const ballAni = BallAni.getInstance();
+                if (ballAni) {
+                    let contactPos: Vec3 = null;
+                    if (contact && (contact as any).getWorldManifold) {
+                        const wm = (contact as any).getWorldManifold();
+                        if (wm && wm.points && wm.points.length > 0) {
+                            contactPos = new Vec3(wm.points[0].x, wm.points[0].y, 0);
+                        }
+                    }
+                    if (!contactPos) {
+                        contactPos = otherNode.worldPosition.clone();
+                    }
+                    ballAni.playImpactEffect(contactPos);
+                }
+                // 禁用碰撞
+                if (contact) {
+                    contact.disabled = true;
+                }
+                return;
+            }
+
+            // 记录撞击历史
+            if (!this.ballHitHistory.has(ballId)) {
+                this.ballHitHistory.set(ballId, []);
+            }
+            const hitHistory = this.ballHitHistory.get(ballId);
+            hitHistory.push(currentTime);
+
+            // 清理过期的撞击记录
+            const timeThreshold = currentTime - this.antiTrapTimeWindow;
+            while (hitHistory.length > 0 && hitHistory[0] < timeThreshold) {
+                hitHistory.shift();
+            }
+
+            // 检查是否达到防围困条件
+            if (hitHistory.length >= this.antiTrapHitThreshold) {
+                // 激活穿透状态,持续0.5秒
+                this.ballPhaseThrough.set(ballId, currentTime + 0.5);
+                // 清空撞击历史
+                hitHistory.length = 0;
+                // 禁用当前碰撞
+                if (contact) {
+                    contact.disabled = true;
+                }
+                console.log(`Ball ${ballId} entered phase-through mode due to frequent collisions`);
+                return;
+            }
+        }
         
         // 计算碰撞世界坐标
         let contactPos: Vec3 = null;
@@ -645,17 +727,6 @@ export class BallController extends Component {
         if (ballAni) {
             ballAni.playImpactEffect(contactPos);
         }
-        
-        // 检查碰撞对象是否为方块,只有方块碰撞才发射子弹
-        const nodeName = otherNode.name;
-        const nodePath = this.getNodePath(otherNode);
-        
-        // 检查是否有Weapon子节点(判断是否为方块)
-        const hasWeaponChild = otherNode.getChildByName('Weapon') !== null;
-        const isBlock = 
-            nodeName.includes('Block') || 
-            nodePath.includes('Block') ||
-            hasWeaponChild;
             
         if (isBlock) {
             // 播放方块撞击动画
@@ -979,6 +1050,9 @@ export class BallController extends Component {
         // 维持所有小球的恒定速度
         this.maintainAllBallsSpeed();
         
+        // 清理过期的防围困状态
+        this.cleanupExpiredAntiTrapStates();
+        
         // 定期检查小球是否接近方块但没有触发碰撞(调试用)
         if (this.activeBall && this.activeBall.isValid) {
             this.debugCheckNearBlocks();
@@ -1248,18 +1322,50 @@ export class BallController extends Component {
         // 清理冷却时间
         this.blockFireCooldown.clear();
         
+        // 清理防围困状态数据
+        this.ballHitHistory.clear();
+        this.ballPhaseThrough.clear();
+        
         // 重置球速
         this.updateBallSpeed();
         
         console.log('[BallController] 球控制器重置完成');
     }
 
+    /**
+     * 清理过期的防围困状态
+     */
+    private cleanupExpiredAntiTrapStates() {
+        const currentTime = performance.now() / 1000;
+        
+        // 清理过期的穿透状态
+        for (const [ballId, endTime] of this.ballPhaseThrough.entries()) {
+            if (currentTime >= endTime) {
+                this.ballPhaseThrough.delete(ballId);
+            }
+        }
+    }
+    
+    /**
+     * 清理单个小球的防围困状态数据
+     * @param ballId 小球的唯一标识符
+     */
+    public cleanupBallAntiTrapState(ballId: string) {
+        this.ballHitHistory.delete(ballId);
+        this.ballPhaseThrough.delete(ballId);
+        console.log(`Cleaned up anti-trap state for ball ${ballId}`);
+    }
+
     onDestroy() {
         // 清理事件监听
         const eventBus = EventBus.getInstance();
         eventBus.off(GameEvents.GAME_PAUSE, this.onGamePauseEvent, this);
         eventBus.off(GameEvents.GAME_RESUME, this.onGameResumeEvent, this);
         eventBus.off(GameEvents.RESET_BALL_CONTROLLER, this.onResetBallControllerEvent, this);
+        
+        // 清理防围困状态数据
+        this.ballHitHistory.clear();
+        this.ballPhaseThrough.clear();
     }
 
     private updateBallSpeed() {

+ 72 - 0
docs/AntiTrapMechanism.md

@@ -0,0 +1,72 @@
+# 小球防围困机制
+
+## 功能描述
+
+防围困机制是为了防止小球被方块围住而无法继续游戏的情况。当小球在规定时间内撞击方块的次数达到阈值时,小球将进入短暂的穿透状态,可以无视碰撞体积穿过方块。
+
+## 配置参数
+
+在 `BallController` 组件中可以配置以下参数:
+
+### antiTrapTimeWindow (防围困检测时间窗口)
+- **类型**: number
+- **默认值**: 2.0 秒
+- **说明**: 用于检测频繁撞击的时间窗口。只有在这个时间窗口内的撞击才会被计入防围困检测。
+
+### antiTrapHitThreshold (防围困撞击次数阈值)
+- **类型**: number
+- **默认值**: 5 次
+- **说明**: 在时间窗口内达到这个撞击次数时,将触发防围困机制。
+
+## 工作原理
+
+1. **撞击记录**: 每当小球撞击方块时,系统会记录撞击时间。
+
+2. **时间窗口清理**: 系统会自动清理超出时间窗口的旧撞击记录。
+
+3. **阈值检测**: 当时间窗口内的撞击次数达到阈值时,触发防围困机制。
+
+4. **穿透状态**: 小球进入穿透状态,持续 0.5 秒,期间可以穿过方块但仍会播放撞击特效。
+
+5. **状态清理**: 穿透状态结束后,小球恢复正常碰撞,撞击历史被清空。
+
+## 技术实现
+
+### 核心数据结构
+
+```typescript
+// 记录每个球的撞击时间历史
+private ballHitHistory: Map<string, number[]> = new Map();
+
+// 记录每个球的穿透结束时间
+private ballPhaseThrough: Map<string, number> = new Map();
+```
+
+### 关键方法
+
+- `onBeginContact()`: 碰撞检测的核心方法,包含防围困逻辑
+- `cleanupExpiredAntiTrapStates()`: 清理过期的穿透状态
+- `cleanupBallAntiTrapState()`: 清理单个小球的防围困状态
+
+## 注意事项
+
+1. **不改变碰撞组**: 该机制不会修改方块的碰撞组设置,只是临时禁用碰撞。
+
+2. **视觉反馈**: 即使在穿透状态下,仍会播放撞击特效,保持视觉一致性。
+
+3. **内存管理**: 系统会自动清理过期数据,防止内存泄漏。
+
+4. **多球支持**: 每个小球都有独立的防围困状态,互不影响。
+
+## 调试信息
+
+当小球进入穿透状态时,控制台会输出调试信息:
+```
+Ball {ballId} entered phase-through mode due to frequent collisions
+```
+
+## 配置建议
+
+- **休闲模式**: 时间窗口 1.5 秒,阈值 3-4 次
+- **普通模式**: 时间窗口 2.0 秒,阈值 5 次(默认)
+- **困难模式**: 时间窗口 2.5 秒,阈值 6-7 次