import { _decorator, Component, Node, Vec2, Vec3, UITransform, Collider2D, Contact2DType, IPhysics2DContact, RigidBody2D, Prefab, instantiate, find, CircleCollider2D } from 'cc'; import { PhysicsManager } from '../Core/PhysicsManager'; import { WeaponBullet, BulletInitData, WeaponConfig } from './WeaponBullet'; const { ccclass, property } = _decorator; @ccclass('BallController') export class BallController extends Component { // 球的预制体 @property({ type: Prefab, tooltip: '拖拽Ball预制体到这里' }) public ballPrefab: Prefab = null; // 已放置方块容器节点 @property({ type: Node, tooltip: '拖拽PlacedBlocks节点到这里(Canvas/GameLevelUI/PlacedBlocks)' }) public placedBlocksContainer: Node = null; // 球的移动速度 @property public speed: number = 10; // 反弹随机偏移最大角度(弧度) @property public maxReflectionRandomness: number = 0.2; // 当前活动的球 private activeBall: Node = null; // 球的方向向量 private direction: Vec2 = new Vec2(); // GameArea区域边界 private gameBounds = { left: 0, right: 0, top: 0, bottom: 0 }; // 球的半径 private radius: number = 0; // 是否已初始化 private initialized: boolean = false; // 子弹预制体 @property({ type: Prefab, tooltip: '拖拽子弹预制体到这里' }) public bulletPrefab: Prefab = null; // 小球是否已开始运动 private ballStarted: boolean = false; // 在类字段区添加 private blockFireCooldown: Map = new Map(); private FIRE_COOLDOWN = 0.05; start() { // 如果没有指定placedBlocksContainer,尝试找到它 if (!this.placedBlocksContainer) { this.placedBlocksContainer = find('Canvas/GameLevelUI/PlacedBlocks'); if (!this.placedBlocksContainer) { // 找不到PlacedBlocks节点,某些功能可能无法正常工作 } } // 只进行初始设置,不创建小球 this.calculateGameBounds(); // 延迟执行一些检查方法,但不创建小球 this.scheduleOnce(() => { this.logCollisionMatrix(); }, 1.0); } // 计算游戏边界(使用GameArea节点) calculateGameBounds() { // 获取GameArea节点 const gameArea = find('Canvas/GameLevelUI/GameArea'); if (!gameArea) { return; } const gameAreaUI = gameArea.getComponent(UITransform); if (!gameAreaUI) { return; } // 获取GameArea的尺寸 const areaWidth = gameAreaUI.width; const areaHeight = gameAreaUI.height; // 获取GameArea的世界坐标位置 const worldPos = gameArea.worldPosition; // 计算GameArea的世界坐标边界 this.gameBounds.left = worldPos.x - areaWidth / 2; this.gameBounds.right = worldPos.x + areaWidth / 2; this.gameBounds.bottom = worldPos.y - areaHeight / 2; this.gameBounds.top = worldPos.y + areaHeight / 2; } // 创建小球 createBall() { if (!this.ballPrefab) { return; } // 如果已经有活动的球,先销毁它 if (this.activeBall && this.activeBall.isValid) { this.activeBall.destroy(); } // 实例化小球 this.activeBall = instantiate(this.ballPrefab); // 将小球添加到GameArea中 const gameArea = find('Canvas/GameLevelUI/GameArea'); if (gameArea) { gameArea.addChild(this.activeBall); } else { this.node.addChild(this.activeBall); } // 随机位置小球 this.positionBallRandomly(); // 设置球的半径 const transform = this.activeBall.getComponent(UITransform); if (transform) { this.radius = transform.width / 2; } else { this.radius = 25; // 默认半径 } // 确保有碰撞组件 this.setupCollider(); // 注意:不在这里初始化方向,等待 startBall() 调用 this.initialized = true; } // 检查所有已放置方块的碰撞体组件 private checkBlockColliders() { if (!this.placedBlocksContainer) { return; } if (!this.placedBlocksContainer.isValid) { return; } const blocks = []; for (let i = 0; i < this.placedBlocksContainer.children.length; i++) { const block = this.placedBlocksContainer.children[i]; if (block.name.includes('Block') || block.getChildByName('B1')) { blocks.push(block); } } let fixedCount = 0; for (let i = 0; i < blocks.length; i++) { const block = blocks[i]; // 检查方块本身的碰撞体 const blockCollider = block.getComponent(Collider2D); if (blockCollider) { // 🔧 自动修复碰撞组设置 if (blockCollider.group !== 2) { blockCollider.group = 2; // 设置为Block组 fixedCount++; } // 确保不是传感器 if (blockCollider.sensor) { blockCollider.sensor = false; } } // 检查B1子节点的碰撞体 const b1Node = block.getChildByName('B1'); if (b1Node) { const b1Collider = b1Node.getComponent(Collider2D); if (b1Collider) { // 🔧 修复B1子节点的碰撞设置 if (b1Collider.group !== 2) { b1Collider.group = 2; // 设置为Block组 fixedCount++; } // 确保B1不是传感器(需要实际碰撞) if (b1Collider.sensor) { b1Collider.sensor = false; } } } // 检查Weapon子节点 const weaponNode = this.findWeaponNode(block); if (weaponNode) { // 武器节点存在 } } } // 随机位置小球 positionBallRandomly() { if (!this.activeBall) return; const transform = this.activeBall.getComponent(UITransform); const ballRadius = transform ? transform.width / 2 : 25; // 计算可生成的范围(考虑小球半径,避免生成在边缘) const minX = this.gameBounds.left + ballRadius + 20; // 额外偏移,避免生成在边缘 const maxX = this.gameBounds.right - ballRadius - 20; const minY = this.gameBounds.bottom + ballRadius + 20; const maxY = this.gameBounds.top - ballRadius - 20; // 获取GameArea节点 const gameArea = find('Canvas/GameLevelUI/GameArea'); if (!gameArea) { return; } // 查找PlacedBlocks节点,它包含所有放置的方块 if (!this.placedBlocksContainer) { this.setRandomPositionDefault(minX, maxX, minY, maxY); return; } if (!this.placedBlocksContainer.isValid) { this.setRandomPositionDefault(minX, maxX, minY, maxY); return; } // 获取所有已放置的方块 const placedBlocks = []; for (let i = 0; i < this.placedBlocksContainer.children.length; i++) { const block = this.placedBlocksContainer.children[i]; // 检查是否是方块节点(通常以Block命名或有特定标识) if (block.name.includes('Block') || block.getChildByName('B1')) { placedBlocks.push(block); } } // 如果没有方块,使用默认随机位置 if (placedBlocks.length === 0) { this.setRandomPositionDefault(minX, maxX, minY, maxY); return; } // 尝试找到一个不与任何方块重叠的位置 let validPosition = false; let attempts = 0; const maxAttempts = 50; // 最大尝试次数 let randomX, randomY; while (!validPosition && attempts < maxAttempts) { // 随机生成位置 randomX = Math.random() * (maxX - minX) + minX; randomY = Math.random() * (maxY - minY) + minY; // 检查是否与任何方块重叠 let overlapping = false; for (const block of placedBlocks) { // 获取方块的世界坐标 const blockWorldPos = block.worldPosition; // 计算小球与方块的距离 const distance = Math.sqrt( Math.pow(randomX - blockWorldPos.x, 2) + Math.pow(randomY - blockWorldPos.y, 2) ); // 获取方块的尺寸 const blockTransform = block.getComponent(UITransform); const blockSize = blockTransform ? Math.max(blockTransform.width, blockTransform.height) / 2 : 50; // 如果距离小于小球半径+方块尺寸的一半+安全距离,认为重叠 const safeDistance = 20; // 额外安全距离 if (distance < ballRadius + blockSize + safeDistance) { overlapping = true; break; } } // 如果没有重叠,找到了有效位置 if (!overlapping) { validPosition = true; } attempts++; } // 如果找不到有效位置,使用默认位置(游戏区域底部中心) if (!validPosition) { randomX = (this.gameBounds.left + this.gameBounds.right) / 2; randomY = this.gameBounds.bottom + ballRadius + 50; // 底部上方50单位 } // 将世界坐标转换为相对于GameArea的本地坐标 const localPos = gameArea.getComponent(UITransform).convertToNodeSpaceAR(new Vec3(randomX, randomY, 0)); this.activeBall.position = localPos; } // 设置默认随机位置 setRandomPositionDefault(minX, maxX, minY, maxY) { // 随机生成位置 const randomX = Math.random() * (maxX - minX) + minX; const randomY = Math.random() * (maxY - minY) + minY; // 将世界坐标转换为相对于GameArea的本地坐标 const gameArea = find('Canvas/GameLevelUI/GameArea'); if (gameArea) { const localPos = gameArea.getComponent(UITransform).convertToNodeSpaceAR(new Vec3(randomX, randomY, 0)); this.activeBall.position = localPos; } else { // 直接设置位置(不太准确,但作为后备) this.activeBall.position = new Vec3(randomX - this.gameBounds.left, randomY - this.gameBounds.bottom, 0); } } // 设置碰撞组件 setupCollider() { if (!this.activeBall) return; // 确保小球有刚体组件 let rigidBody = this.activeBall.getComponent(RigidBody2D); if (!rigidBody) { rigidBody = this.activeBall.addComponent(RigidBody2D); rigidBody.type = 2; // Dynamic rigidBody.gravityScale = 0; // 不受重力影响 rigidBody.enabledContactListener = true; // 启用碰撞监听 rigidBody.fixedRotation = true; // 固定旋转 rigidBody.allowSleep = false; // 不允许休眠 rigidBody.linearDamping = 0; // 无线性阻尼,保持速度不衰减 rigidBody.angularDamping = 0; // 无角阻尼 } else { // 确保已有的刚体组件设置正确 rigidBody.enabledContactListener = true; rigidBody.gravityScale = 0; rigidBody.linearDamping = 0; // 确保无线性阻尼,保持速度不衰减 rigidBody.angularDamping = 0; // 确保无角阻尼 rigidBody.allowSleep = false; // 不允许休眠 } // 确保小球有碰撞组件 let collider = this.activeBall.getComponent(CircleCollider2D); if (!collider) { collider = this.activeBall.addComponent(CircleCollider2D); collider.radius = this.radius || 25; // 使用已计算的半径或默认值 collider.tag = 1; // 小球标签 collider.group = 1; // 碰撞组1 - Ball组 collider.sensor = false; // 非传感器(实际碰撞) collider.friction = 0; // 无摩擦 collider.restitution = 1; // 完全弹性碰撞 } else { // 确保已有的碰撞组件设置正确 collider.sensor = false; collider.restitution = 1; collider.group = 1; // 确保是Ball组 collider.tag = 1; // 确保标签正确 } // === 使用全局回调监听器 === const physics = PhysicsManager.getInstance()?.getSystem(); if (physics) { // 先移除旧监听,避免重复注册 physics.off(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); physics.off(Contact2DType.END_CONTACT, this.onEndContact, this); physics.off(Contact2DType.PRE_SOLVE, this.onPreSolve, this); physics.off(Contact2DType.POST_SOLVE, this.onPostSolve, this); physics.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); physics.on(Contact2DType.END_CONTACT, this.onEndContact, this); physics.on(Contact2DType.PRE_SOLVE, this.onPreSolve, this); physics.on(Contact2DType.POST_SOLVE, this.onPostSolve, this); } } // 碰撞回调 - 简化版本用于测试 onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) { // Debug logs removed // 判断哪个是小球,哪个是方块 let ballNode: Node = null; let blockNode: Node = null; // 检查self是否为小球(组1) if (selfCollider.group === 1) { ballNode = selfCollider.node; blockNode = otherCollider.node; } // 检查other是否为小球(组1) else if (otherCollider.group === 1) { ballNode = otherCollider.node; blockNode = selfCollider.node; } // 如果没有找到小球,跳过处理 if (!ballNode || !blockNode) { // Debug log removed return; } // 检查碰撞对象是否为方块 const nodeName = blockNode.name; const nodePath = this.getNodePath(blockNode); // 检查是否有Weapon子节点(判断是否为方块) const hasWeaponChild = blockNode.getChildByName('Weapon') !== null; const isBlock = nodeName.includes('Block') || nodePath.includes('Block') || hasWeaponChild; if (isBlock) { // trigger bullet without verbose logging // 计算碰撞世界坐标,默认用接触点 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 = blockNode.worldPosition.clone(); } const now = performance.now(); const lastTime = this.blockFireCooldown.get(blockNode.uuid) || 0; if (now - lastTime > this.FIRE_COOLDOWN * 1000) { this.blockFireCooldown.set(blockNode.uuid, now); this.fireBulletAt(blockNode, contactPos); } } } // 碰撞结束事件 - 简化版本 onEndContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) { // Debug log removed } // 碰撞预处理事件 - 简化版本 onPreSolve(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) { // console.log('⚙️ 碰撞预处理:', otherCollider.node.name); } // 碰撞后处理事件 - 简化版本 onPostSolve(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) { // console.log('✅ 碰撞后处理:', otherCollider.node.name); } // 计算反射向量 calculateReflection(direction: Vec2, normal: Vec2): Vec2 { // 使用反射公式: R = V - 2(V·N)N const dot = direction.x * normal.x + direction.y * normal.y; const reflection = new Vec2( direction.x - 2 * dot * normal.x, direction.y - 2 * dot * normal.y ); reflection.normalize(); // 添加一些随机性,避免重复的反弹路径 const randomAngle = (Math.random() - 0.5) * this.maxReflectionRandomness; // 随机角度 const cos = Math.cos(randomAngle); const sin = Math.sin(randomAngle); // 应用随机旋转 const randomizedReflection = new Vec2( reflection.x * cos - reflection.y * sin, reflection.x * sin + reflection.y * cos ); // 确保反射方向不会太接近水平或垂直方向 // 这有助于避免球在水平或垂直方向上来回反弹 const minAngleFromAxis = 0.1; // 约5.7度 // 检查是否接近水平方向 if (Math.abs(randomizedReflection.y) < minAngleFromAxis) { // 调整y分量,使其远离水平方向 const sign = randomizedReflection.y >= 0 ? 1 : -1; randomizedReflection.y = sign * (minAngleFromAxis + Math.random() * 0.1); // 重新归一化 randomizedReflection.normalize(); } // 检查是否接近垂直方向 if (Math.abs(randomizedReflection.x) < minAngleFromAxis) { // 调整x分量,使其远离垂直方向 const sign = randomizedReflection.x >= 0 ? 1 : -1; randomizedReflection.x = sign * (minAngleFromAxis + Math.random() * 0.1); // 重新归一化 randomizedReflection.normalize(); } randomizedReflection.normalize(); // Debug log removed return randomizedReflection; } /** * 从方块武器发射子弹攻击敌人 - 重构版本 * 现在直接创建子弹实例并使用BulletController的实例方法 * @param blockNode 激活的方块节点 */ fireBullet(blockNode: Node) { // Debug logs removed // 检查子弹预制体是否存在 if (!this.bulletPrefab) { return; } // 查找方块中的Weapon节点 const weaponNode = this.findWeaponNode(blockNode); if (!weaponNode) { const blockWorldPos = blockNode.worldPosition; const weaponConfig2: WeaponConfig | null = (blockNode as any)['weaponConfig'] || null; this.createAndFireBullet(blockWorldPos, weaponConfig2); return; } // 获取武器的世界坐标作为发射位置 let firePosition: Vec3; try { firePosition = weaponNode.worldPosition; } catch (error) { // 备用方案:使用方块坐标 firePosition = blockNode.worldPosition; } // 创建并发射子弹 const weaponConfig: WeaponConfig | null = (blockNode as any)['weaponConfig'] || null; this.createAndFireBullet(firePosition, weaponConfig); } /** * 创建并发射子弹 - 使用新的WeaponBullet系统 * @param firePosition 发射位置(世界坐标) * @param weaponConfig 武器配置 */ private createAndFireBullet(firePosition: Vec3, weaponConfig: WeaponConfig | null) { // 确保武器配置加载 WeaponBullet.loadWeaponsData().then(() => { // 默认使用毛豆射手配置,后续可以根据方块类型动态选择 const defaultWeaponId = 'pea_shooter'; const finalConfig = weaponConfig || WeaponBullet.getWeaponConfig(defaultWeaponId); if (!finalConfig) { return; } // 创建子弹初始化数据 const initData: BulletInitData = { weaponId: finalConfig.id, firePosition: firePosition, autoTarget: true, weaponConfig: finalConfig }; // 验证初始化数据 if (!WeaponBullet.validateInitData(initData)) { return; } // 判断是否为多发子弹(散射/连发等) const countCfg = finalConfig.bulletConfig.count; const isMultiShot = countCfg && countCfg.type !== 'single' && countCfg.amount > 1; // 查找GameArea(子弹统一添加到此节点) const gameArea = find('Canvas/GameLevelUI/GameArea'); if (!gameArea) { return; } if (isMultiShot) { // 使用批量创建逻辑 const bullets = WeaponBullet.createBullets(initData, this.bulletPrefab as any); bullets.forEach(b => { gameArea.addChild(b); }); } else { // 单发逻辑(沿用原流程) const bullet = instantiate(this.bulletPrefab); if (!bullet) { return; } gameArea.addChild(bullet); let weaponBullet = bullet.getComponent(WeaponBullet); if (!weaponBullet) { weaponBullet = bullet.addComponent(WeaponBullet); } weaponBullet.init(initData); } }).catch(error => { // 武器配置加载失败 }); } // 递归查找Weapon节点 private findWeaponNode(node: Node): Node | null { // logs removed // 先检查当前节点是否有Weapon子节点 const weaponNode = node.getChildByName('Weapon'); if (weaponNode) { return weaponNode; } // 如果没有,递归检查所有子节点 for (let i = 0; i < node.children.length; i++) { const child = node.children[i]; const foundWeapon = this.findWeaponNode(child); if (foundWeapon) { return foundWeapon; } } // 如果都没找到,返回null return null; } // 辅助方法:打印节点结构 private logNodeStructure(node: Node, depth: number) { const indent = ' '.repeat(depth); for (let i = 0; i < node.children.length; i++) { this.logNodeStructure(node.children[i], depth + 1); } } // 获取节点的完整路径 private getNodePath(node: Node): string { let path = node.name; let current = node; while (current.parent) { current = current.parent; path = current.name + '/' + path; } return path; } update(dt: number) { // 只有当小球已启动时才执行运动逻辑 if (!this.ballStarted || !this.initialized || !this.activeBall || !this.activeBall.isValid) { return; } // 使用刚体组件控制小球移动,而不是直接设置位置 const rigidBody = this.activeBall.getComponent(RigidBody2D); if (rigidBody) { // 获取当前速度并确保方向向量与实际速度方向一致 const currentVelocity = rigidBody.linearVelocity; const speed = Math.sqrt(currentVelocity.x * currentVelocity.x + currentVelocity.y * currentVelocity.y); // 确保方向向量与实际速度方向一致 if (speed > 1.0) { this.direction.x = currentVelocity.x / speed; this.direction.y = currentVelocity.y / speed; } // 如果速度过低,重新设置速度以维持恒定运动 if (speed < this.speed * 0.9) { rigidBody.linearVelocity = new Vec2( this.direction.x * this.speed, this.direction.y * this.speed ); } // 定期检查小球是否接近方块但没有触发碰撞(调试用) this.debugCheckNearBlocks(); } } // 调试方法:检查小球是否接近方块但没有触发物理碰撞 private debugCheckCounter = 0; private debugCheckNearBlocks() { this.debugCheckCounter++; // 每60帧(约1秒)检查一次 if (this.debugCheckCounter % 60 !== 0) return; const ballPos = this.activeBall.worldPosition; if (!this.placedBlocksContainer || !this.placedBlocksContainer.isValid) return; let nearestDistance = Infinity; let nearestBlock = null; for (let i = 0; i < this.placedBlocksContainer.children.length; i++) { const block = this.placedBlocksContainer.children[i]; if (block.name.includes('Block') || block.getChildByName('B1')) { const blockPos = block.worldPosition; const distance = Math.sqrt( Math.pow(ballPos.x - blockPos.x, 2) + Math.pow(ballPos.y - blockPos.y, 2) ); if (distance < nearestDistance) { nearestDistance = distance; nearestBlock = block; } } } if (nearestBlock && nearestDistance < 100) { // 检查小球的碰撞体状态 const ballCollider = this.activeBall.getComponent(Collider2D); if (ballCollider) { if (ballCollider instanceof CircleCollider2D) { // 小球碰撞半径检查 } } // 检查小球的刚体状态 const ballRigidBody = this.activeBall.getComponent(RigidBody2D); if (ballRigidBody) { // 刚体状态检查 } // 检查最近的方块碰撞体 const blockCollider = nearestBlock.getComponent(Collider2D); if (blockCollider) { // 方块碰撞体检查 } } } // 检查碰撞矩阵 private checkCollisionMatrix(group1: number, group2: number): boolean { // 根据项目配置检查碰撞矩阵 // "collisionMatrix": { "0": 3, "1": 39, "2": 6, "3": 16, "4": 40, "5": 18 } const collisionMatrix: { [key: string]: number } = { "0": 3, "1": 39, "2": 6, "3": 16, "4": 40, "5": 18 }; const mask1 = collisionMatrix[group1.toString()]; const mask2 = collisionMatrix[group2.toString()]; // 检查group1是否能与group2碰撞 const canCollide1to2 = mask1 && (mask1 & (1 << group2)) !== 0; // 检查group2是否能与group1碰撞 const canCollide2to1 = mask2 && (mask2 & (1 << group1)) !== 0; return canCollide1to2 && canCollide2to1; } // 初始化方向 initializeDirection() { // 随机初始方向 const angle = Math.random() * Math.PI * 2; // 0-2π之间的随机角度 this.direction.x = Math.cos(angle); this.direction.y = Math.sin(angle); this.direction.normalize(); // 设置初始速度 if (this.activeBall) { const rigidBody = this.activeBall.getComponent(RigidBody2D); if (rigidBody) { rigidBody.linearVelocity = new Vec2( this.direction.x * this.speed, this.direction.y * this.speed ); } } } // 初始化球的参数 - 公开方法,供GameManager调用 public initialize() { this.calculateGameBounds(); this.createBall(); // 检查方块碰撞体(延迟执行,确保方块已放置) this.scheduleOnce(() => { this.checkBlockColliders(); }, 0.5); } // 启动小球 - 公开方法,在确定按钮点击后调用 public startBall() { // 如果还没有初始化,先初始化 if (!this.initialized) { this.initialize(); } // 确保小球存在且有效 if (!this.activeBall || !this.activeBall.isValid) { this.createBall(); } // 重新定位小球,避免与方块重叠 this.positionBallRandomly(); // 确保物理组件设置正确 this.setupCollider(); // 初始化运动方向并开始运动 this.initializeDirection(); // 设置运动状态 this.ballStarted = true; } // 输出碰撞矩阵调试信息 private logCollisionMatrix() { // 根据项目配置检查碰撞矩阵 // "collisionMatrix": { "0": 3, "1": 39, "2": 6, "3": 16, "4": 40, "5": 18 } const collisionMatrix: { [key: string]: number } = { "0": 3, "1": 39, "2": 6, "3": 16, "4": 40, "5": 18 }; // 测试Ball组(1)和Block组(2)是否能碰撞 const ballMask = collisionMatrix["1"]; // 39 const blockMask = collisionMatrix["2"]; // 6 const ballCanCollideWithBlock = (ballMask & (1 << 2)) !== 0; // 检查Ball能否与组2碰撞 const blockCanCollideWithBall = (blockMask & (1 << 1)) !== 0; // 检查Block能否与组1碰撞 } /** * 从给定世界坐标发射子弹 */ private fireBulletAt(blockNode: Node, fireWorldPos: Vec3) { // 检查子弹预制体是否存在 if (!this.bulletPrefab) { return; } // 直接使用碰撞世界坐标作为发射点 const weaponConfig: WeaponConfig | null = (blockNode as any)['weaponConfig'] || null; this.createAndFireBullet(fireWorldPos, weaponConfig); } }