| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- 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));
- }
- }
|