import { _decorator, Component, Node, Vec2, Vec3, RigidBody2D, CircleCollider2D, find, UITransform } from 'cc'; const { ccclass, property } = _decorator; @ccclass('BallInstance') export class BallInstance extends Component { @property({ tooltip: '基础速度,用于防围困时的冲量大小参考' }) public baseSpeed: number = 60; @property({ tooltip: '目标速度(不在此组件内强制维持,用于参考)' }) public currentSpeed: number = 60; @property({ tooltip: '是否启用振荡检测与增强防围困' }) public enableOscillationDetection: boolean = true; @property({ tooltip: '振荡检测时间窗口(秒)' }) public oscillationTimeWindow: number = 3.0; @property({ tooltip: '方向改变阈值(次)' }) public directionChangeThreshold: number = 4; @property({ tooltip: '位置历史记录大小' }) public positionHistorySize: number = 20; @property({ tooltip: '振荡距离阈值(像素)' }) public oscillationDistanceThreshold: number = 100; @property({ tooltip: '测试模式(启用日志)' }) public testMode: boolean = true; // —— 位置检查(实例级调度) —— @property({ tooltip: '启用位置检查与救援(实例级)' }) public enableBallPositionCheck: boolean = true; @property({ tooltip: '位置检查间隔(秒)(实例级)' }) public positionCheckInterval: number = 2.0; private _positionCheckElapsed: number = 0; // —— 安全区域与安全放置配置(由控制器注入) —— @property({ tooltip: '与边缘保持的最小距离' }) public edgeOffset: number = 16; @property({ tooltip: '与方块保持的最小安全距离' }) public safeDistance: number = 36; @property({ tooltip: '球半径(用于避让计算)' }) public ballRadius: number = 12; @property({ tooltip: '放置尝试最大次数' }) public maxAttempts: number = 24; @property({ tooltip: '位置检查边界扩展(像素)' }) public positionCheckBoundaryExtension: number = 24; @property({ type: Node, tooltip: '已放置方块容器(Canvas/GameLevelUI/GameArea/PlacedBlocks)' }) public placedBlocksContainer: Node | null = null; private posHistory: Vec2[] = []; private dirHistory: Vec2[] = []; private lastDirectionChange: number = 0; private directionChangeCount: number = 0; private oscillationAxis: 'horizontal' | 'vertical' | 'none' = 'none'; private oscillationStartTime: number = 0; update(dt: number) { if (!this.enableOscillationDetection) return; const ball = this.node; if (!ball || !ball.isValid) return; const rigidBody = ball.getComponent(RigidBody2D); if (!rigidBody) return; const currentTime = Date.now() / 1000; const pos = ball.getWorldPosition(); const vel = rigidBody.linearVelocity; const dir = new Vec2(vel.x, vel.y).normalize(); // 初始化 if (this.posHistory.length === 0) { this.lastDirectionChange = currentTime; this.directionChangeCount = 0; this.oscillationAxis = 'none'; this.oscillationStartTime = 0; } // 位置历史 this.posHistory.push(new Vec2(pos.x, pos.y)); if (this.posHistory.length > this.positionHistorySize) { this.posHistory.shift(); } // 方向历史 this.dirHistory.push(dir.clone()); if (this.dirHistory.length > this.positionHistorySize) { this.dirHistory.shift(); } // 检测方向改变(点积 < 0 认为显著改变) if (this.dirHistory.length >= 2) { const prev = this.dirHistory[this.dirHistory.length - 2]; const dot = Vec2.dot(prev, dir); if (dot < 0) { this.directionChangeCount++; this.lastDirectionChange = currentTime; if (this.testMode) { // console.log(`[BallInstance] 方向改变,累计: ${this.directionChangeCount}`); } } } // 时间窗口外重置 const timeSinceLast = currentTime - this.lastDirectionChange; if (timeSinceLast > this.oscillationTimeWindow) { this.directionChangeCount = 0; this.oscillationAxis = 'none'; return; } // 达到方向改变阈值,分析振荡轴向并触发 if (this.directionChangeCount >= this.directionChangeThreshold) { const axis = this.analyzeOscillationAxis(this.posHistory); if (axis !== 'none' && this.oscillationAxis === 'none') { this.oscillationAxis = axis; this.oscillationStartTime = currentTime; if (this.testMode) { console.log(`[BallInstance] 检测到${axis === 'horizontal' ? '水平' : '垂直'}振荡,触发增强防围困`); } this.triggerEnhancedAntiTrap(axis); } } // 实例级位置检查调度 if (this.enableBallPositionCheck) { this._positionCheckElapsed += dt; if (this._positionCheckElapsed >= this.positionCheckInterval) { this.checkAndRescueIfNeeded(); this._positionCheckElapsed = 0; } } } private analyzeOscillationAxis(history: Vec2[]): 'horizontal' | 'vertical' | 'none' { if (!history || history.length < 10) return 'none'; const recent = history.slice(-10); let minX = recent[0].x, maxX = recent[0].x; let minY = recent[0].y, maxY = recent[0].y; for (const p of recent) { minX = Math.min(minX, p.x); maxX = Math.max(maxX, p.x); minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y); } const xRange = maxX - minX; const yRange = maxY - minY; if (xRange > this.oscillationDistanceThreshold && yRange < this.oscillationDistanceThreshold / 2) { return 'horizontal'; } if (yRange > this.oscillationDistanceThreshold && xRange < this.oscillationDistanceThreshold / 2) { return 'vertical'; } return 'none'; } private triggerEnhancedAntiTrap(axis: 'horizontal' | 'vertical') { const rigidBody = this.node.getComponent(RigidBody2D); if (!rigidBody) return; if (axis === 'horizontal') { const verticalImpulse = (Math.random() - 0.5) * this.baseSpeed * 0.8; const newVel = new Vec2(rigidBody.linearVelocity.x * 0.3, verticalImpulse); rigidBody.linearVelocity = newVel; if (this.testMode) { console.log(`[BallInstance] 水平振荡 -> 垂直冲量: ${verticalImpulse.toFixed(2)}`); } } else { const horizontalImpulse = (Math.random() - 0.5) * this.baseSpeed * 0.8; const newVel = new Vec2(horizontalImpulse, rigidBody.linearVelocity.y * 0.3); rigidBody.linearVelocity = newVel; if (this.testMode) { console.log(`[BallInstance] 垂直振荡 -> 水平冲量: ${horizontalImpulse.toFixed(2)}`); } } } // ---------- Safe Area Logic (migrated) ---------- private getGameBounds(): { left: number; right: number; top: number; bottom: number; gameArea: Node; ui: UITransform } | null { const gameArea = find('Canvas/GameLevelUI/GameArea'); if (!gameArea) return null; const ui = gameArea.getComponent(UITransform); if (!ui) return null; const wpos = gameArea.worldPosition; const halfW = ui.width / 2; const halfH = ui.height / 2; return { left: wpos.x - halfW, right: wpos.x + halfW, bottom: wpos.y - halfH, top: wpos.y + halfH, gameArea, ui, }; } public checkInBounds(): boolean { const bounds = this.getGameBounds(); if (!bounds) return true; const ext = this.positionCheckBoundaryExtension; const pos = this.node.worldPosition; const inX = pos.x >= (bounds.left - ext) && pos.x <= (bounds.right + ext); const inY = pos.y >= (bounds.bottom - ext) && pos.y <= (bounds.top + ext); return inX && inY; } public rescueToGameArea(): void { const bounds = this.getGameBounds(); if (!bounds) return; this.positionSafely(bounds); const rigidBody = this.node.getComponent(RigidBody2D); if (rigidBody) { const angle = Math.random() * Math.PI * 2; const dir = new Vec2(Math.cos(angle), Math.sin(angle)).normalize(); rigidBody.linearVelocity = new Vec2(dir.x * this.currentSpeed, dir.y * this.currentSpeed); } } public checkAndRescueIfNeeded(): boolean { if (this.checkInBounds()) return false; this.rescueToGameArea(); return true; } /** * 对外公开:在游戏区域内安全随机摆放(考虑方块避让) */ public placeRandomlyInGameArea(): void { const bounds = this.getGameBounds(); if (!bounds) return; this.positionSafely(bounds); } private positionSafely(bounds: { left: number; right: number; top: number; bottom: number; gameArea: Node; ui: UITransform; }): void { const minX = bounds.left + this.ballRadius + this.edgeOffset; const maxX = bounds.right - this.ballRadius - this.edgeOffset; const minY = bounds.bottom + this.ballRadius + this.edgeOffset; const maxY = bounds.top - this.ballRadius - this.edgeOffset; if (!this.placedBlocksContainer || !this.placedBlocksContainer.isValid) { this.setRandomPositionDefault(minX, maxX, minY, maxY, bounds); return; } const placedBlocks: Node[] = []; for (const child of this.placedBlocksContainer.children) { if (child.name.includes('Block') || child.getChildByName('B1')) { placedBlocks.push(child); } } if (placedBlocks.length === 0) { this.setRandomPositionDefault(minX, maxX, minY, maxY, bounds); return; } let attempts = 0; const maxAttempts = this.maxAttempts; while (attempts < maxAttempts) { const rx = Math.random() * (maxX - minX) + minX; const ry = Math.random() * (maxY - minY) + minY; const worldPos = new Vec3(rx, ry, 0); let overlapping = false; for (const block of placedBlocks) { const bpos = block.worldPosition; const dx = rx - bpos.x; const dy = ry - bpos.y; const dist = Math.sqrt(dx * dx + dy * dy); const bt = block.getComponent(UITransform); const bsize = bt ? Math.max(bt.width, bt.height) / 2 : this.safeDistance; if (dist < this.ballRadius + bsize + this.safeDistance) { overlapping = true; break; } } if (!overlapping) { const local = bounds.ui.convertToNodeSpaceAR(worldPos); this.node.setPosition(local); return; } attempts++; } // 兜底:底部中心 const cx = (bounds.left + bounds.right) / 2; const cy = bounds.bottom + this.ballRadius + this.safeDistance; const fallback = new Vec3(cx, cy, 0); this.node.setPosition(bounds.ui.convertToNodeSpaceAR(fallback)); } private setRandomPositionDefault(minX: number, maxX: number, minY: number, maxY: number, bounds: { ui: UITransform }): void { const rx = Math.random() * (maxX - minX) + minX; const ry = Math.random() * (maxY - minY) + minY; const worldPos = new Vec3(rx, ry, 0); this.node.setPosition(bounds.ui.convertToNodeSpaceAR(worldPos)); } }