WeaponBullet.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. import { _decorator, Component, Node, Vec2, Vec3, RigidBody2D, Collider2D, Contact2DType, IPhysics2DContact, find, Prefab, instantiate, UITransform, resources, sp } from 'cc';
  2. import { WeaponBlockExample } from './WeaponBlockExample';
  3. const { ccclass, property } = _decorator;
  4. /**
  5. * WeaponBullet - 统一的子弹控制器
  6. *
  7. * 整合了原 BulletController 的所有功能:
  8. * 1. 自动瞄准最近敌人
  9. * 2. 物理属性设置
  10. * 3. 生命周期管理
  11. * 4. 特效系统
  12. * 5. 多种子弹行为
  13. *
  14. * 支持的子弹行为:
  15. * - explosive: 击中敌人立即销毁并播放hitEffect
  16. * - piercing: 击中敌人不销毁,持续飞行
  17. * - ricochet: 弹射子弹,可设置弹射次数
  18. * - boomerang: 回旋镖,延迟后返回
  19. * - area_burn: 区域灼烧效果
  20. * - homing: 导弹追踪
  21. */
  22. export type BulletBehaviour = 'explosive' | 'piercing' | 'ricochet' | 'boomerang' | 'area_burn' | 'homing' | 'normal';
  23. export interface BulletInitData {
  24. behavior: BulletBehaviour;
  25. speed: number;
  26. damage: number;
  27. hitEffect?: string; // resources 路径
  28. trailEffect?: string; // resources 路径
  29. lifetime?: number; // 子弹生存时间
  30. ricochetCount?: number; // ricochet 弹射次数
  31. ricochetAngle?: number; // 每次弹射随机角度 (deg)
  32. arcHeight?: number; // boomerang
  33. returnDelay?: number;
  34. burnDuration?: number; // area burn
  35. homingDelay?: number; // homing missile
  36. homingStrength?: number;
  37. // 从 BulletController 迁移的配置
  38. firePosition?: Vec3; // 发射位置(世界坐标)
  39. autoTarget?: boolean; // 是否自动瞄准最近敌人
  40. }
  41. @ccclass('WeaponBullet')
  42. export class WeaponBullet extends Component {
  43. // === 基础配置 ===
  44. @property
  45. public speed: number = 300;
  46. @property
  47. public damage: number = 1;
  48. @property
  49. public lifetime: number = 5;
  50. @property({ tooltip: '击中特效路径(resources 相对路径,例如 Animation/tx/tx0001)' })
  51. public hitEffectPath: string = '';
  52. @property({ tooltip: '拖尾特效路径' })
  53. public trailEffectPath: string = '';
  54. @property({ tooltip: '是否自动瞄准最近敌人' })
  55. public autoTarget: boolean = true;
  56. // === 子弹行为配置 ===
  57. private behavior: BulletBehaviour = 'explosive';
  58. private ricochetLeft = 0;
  59. private ricochetAngle = 30; // deg
  60. // boomerang
  61. private boomerangTimer = 0;
  62. private boomerangReturnDelay = 1;
  63. private initialDir: Vec3 = new Vec3();
  64. // area burn
  65. private burnDuration = 5;
  66. // homing
  67. private homingDelay = 0.3;
  68. private homingStrength = 0.8;
  69. private homingTimer = 0;
  70. // === 从 BulletController 迁移的属性 ===
  71. private targetEnemy: Node = null;
  72. private rigidBody: RigidBody2D = null;
  73. private lifeTimer: number = 0;
  74. private direction: Vec3 = new Vec3(1, 0, 0);
  75. private firePosition: Vec3 = null;
  76. private isInitialized: boolean = false;
  77. private needsPhysicsSetup: boolean = false;
  78. /**
  79. * 初始化子弹配置
  80. */
  81. public init(cfg: BulletInitData) {
  82. this.behavior = cfg.behavior || 'explosive';
  83. this.speed = cfg.speed || this.speed;
  84. this.damage = cfg.damage || this.damage;
  85. this.hitEffectPath = cfg.hitEffect || this.hitEffectPath;
  86. this.trailEffectPath = cfg.trailEffect || this.trailEffectPath;
  87. this.lifetime = cfg.lifetime !== undefined ? cfg.lifetime : this.lifetime;
  88. this.autoTarget = cfg.autoTarget !== undefined ? cfg.autoTarget : this.autoTarget;
  89. // 子弹行为配置
  90. if (cfg.ricochetCount !== undefined) this.ricochetLeft = cfg.ricochetCount;
  91. if (cfg.ricochetAngle) this.ricochetAngle = cfg.ricochetAngle;
  92. if (cfg.returnDelay) this.boomerangReturnDelay = cfg.returnDelay;
  93. if (cfg.burnDuration) this.burnDuration = cfg.burnDuration;
  94. if (cfg.homingDelay) {
  95. this.homingDelay = cfg.homingDelay;
  96. this.homingTimer = cfg.homingDelay;
  97. }
  98. if (cfg.homingStrength) this.homingStrength = cfg.homingStrength;
  99. // 发射位置设置
  100. if (cfg.firePosition) {
  101. this.setFirePosition(cfg.firePosition);
  102. }
  103. // 兼容性处理
  104. if (this.behavior === 'normal') this.behavior = 'explosive';
  105. console.log('🔫 WeaponBullet 初始化完成:', {
  106. behavior: this.behavior,
  107. speed: this.speed,
  108. damage: this.damage,
  109. autoTarget: this.autoTarget
  110. });
  111. }
  112. /**
  113. * 设置子弹的发射位置(从 BulletController 迁移)
  114. * @param position 发射位置(世界坐标)
  115. */
  116. public setFirePosition(position: Vec3) {
  117. this.firePosition = position.clone();
  118. this.needsPhysicsSetup = true;
  119. console.log('🎯 子弹发射位置已设置:', this.firePosition);
  120. }
  121. /**
  122. * 设置特效路径(从 BulletController 迁移)
  123. */
  124. public setEffectPaths(hit: string | null, trail?: string | null) {
  125. if (hit) this.hitEffectPath = hit;
  126. if (trail) this.trailEffectPath = trail;
  127. }
  128. onLoad() {
  129. this.rigidBody = this.getComponent(RigidBody2D);
  130. const collider = this.getComponent(Collider2D);
  131. if (collider) {
  132. collider.on(Contact2DType.BEGIN_CONTACT, this.onHit, this);
  133. }
  134. }
  135. start() {
  136. console.log('🚀 WeaponBullet start()开始...');
  137. // 设置生命周期
  138. this.lifeTimer = this.lifetime;
  139. // 如果需要设置物理属性,延迟处理
  140. if (this.needsPhysicsSetup) {
  141. this.scheduleOnce(() => {
  142. this.setupPhysics();
  143. }, 0.05);
  144. } else {
  145. // 直接初始化
  146. this.setupPhysics();
  147. }
  148. // 添加拖尾特效
  149. if (this.behavior === 'piercing' && this.trailEffectPath) {
  150. this.attachTrail();
  151. }
  152. console.log('✅ WeaponBullet start()完成');
  153. }
  154. /**
  155. * 设置物理属性(从 BulletController 迁移)
  156. */
  157. private setupPhysics() {
  158. console.log('🔧 开始设置子弹物理属性...');
  159. // 确保有刚体组件
  160. if (!this.rigidBody) {
  161. console.error('❌ 子弹预制体缺少RigidBody2D组件');
  162. return;
  163. }
  164. const collider = this.getComponent(Collider2D);
  165. if (!collider) {
  166. console.error('❌ 子弹预制体缺少Collider2D组件');
  167. return;
  168. }
  169. // 设置物理属性
  170. this.rigidBody.enabledContactListener = true;
  171. this.rigidBody.gravityScale = 0; // 不受重力影响
  172. this.rigidBody.linearDamping = 0; // 无阻尼
  173. this.rigidBody.angularDamping = 0; // 无角阻尼
  174. console.log('✅ 物理组件设置成功');
  175. // 设置子弹位置
  176. if (this.firePosition) {
  177. this.setPositionInGameArea();
  178. }
  179. // 初始化方向和速度
  180. this.initializeDirection();
  181. this.isInitialized = true;
  182. console.log('✅ 子弹物理属性设置完成');
  183. }
  184. /**
  185. * 在GameArea中设置子弹位置(从 BulletController 迁移)
  186. */
  187. private setPositionInGameArea() {
  188. const gameArea = find('Canvas/GameLevelUI/GameArea');
  189. if (gameArea) {
  190. const gameAreaTransform = gameArea.getComponent(UITransform);
  191. if (gameAreaTransform) {
  192. const localPos = gameAreaTransform.convertToNodeSpaceAR(this.firePosition);
  193. this.node.position = localPos;
  194. console.log('✅ 子弹位置已设置:', localPos);
  195. }
  196. }
  197. }
  198. /**
  199. * 初始化子弹方向(从 BulletController 迁移并增强)
  200. */
  201. private initializeDirection() {
  202. console.log('🎯 开始初始化子弹方向...');
  203. // 如果启用自动瞄准,寻找最近的敌人
  204. if (this.autoTarget) {
  205. this.findNearestEnemy();
  206. }
  207. // 设置方向
  208. if (this.targetEnemy && this.autoTarget) {
  209. this.setDirectionToTarget();
  210. } else {
  211. // 随机方向或使用默认方向
  212. const randomAngle = Math.random() * Math.PI * 2;
  213. this.direction = new Vec3(Math.cos(randomAngle), Math.sin(randomAngle), 0);
  214. console.log('🎲 使用随机方向');
  215. }
  216. // 保存初始方向(用于boomerang)
  217. this.initialDir.set(this.direction);
  218. // 应用速度
  219. this.applyVelocity();
  220. }
  221. /**
  222. * 寻找最近的敌人(从 BulletController 迁移)
  223. */
  224. private findNearestEnemy() {
  225. const enemyContainer = find('Canvas/GameLevelUI/enemyContainer');
  226. if (!enemyContainer) {
  227. console.log('❌ 未找到敌人容器');
  228. return;
  229. }
  230. const enemies = enemyContainer.children.filter(child =>
  231. child.active &&
  232. (child.name.toLowerCase().includes('enemy') ||
  233. child.getComponent('EnemyInstance') !== null)
  234. );
  235. if (enemies.length === 0) {
  236. console.log('❌ 没有找到敌人');
  237. return;
  238. }
  239. let nearestEnemy: Node = null;
  240. let nearestDistance = Infinity;
  241. const bulletPos = this.node.worldPosition;
  242. for (const enemy of enemies) {
  243. const distance = Vec3.distance(bulletPos, enemy.worldPosition);
  244. if (distance < nearestDistance) {
  245. nearestDistance = distance;
  246. nearestEnemy = enemy;
  247. }
  248. }
  249. if (nearestEnemy) {
  250. this.targetEnemy = nearestEnemy;
  251. console.log(`🎯 锁定目标: ${nearestEnemy.name}`);
  252. }
  253. }
  254. /**
  255. * 设置朝向目标的方向(从 BulletController 迁移)
  256. */
  257. private setDirectionToTarget() {
  258. if (!this.targetEnemy) return;
  259. const targetPos = this.targetEnemy.worldPosition;
  260. const currentPos = this.node.worldPosition;
  261. this.direction = targetPos.clone().subtract(currentPos).normalize();
  262. console.log('🎯 方向已设置,朝向目标敌人');
  263. }
  264. /**
  265. * 应用速度(从 BulletController 迁移并增强)
  266. */
  267. private applyVelocity() {
  268. if (!this.rigidBody || !this.direction) {
  269. console.error('❌ 无法应用速度:缺少刚体或方向');
  270. return;
  271. }
  272. // 覆盖速度值(全局控制)
  273. const weaponGlobal = WeaponBlockExample.getInstance && WeaponBlockExample.getInstance();
  274. if (weaponGlobal) {
  275. this.speed = weaponGlobal.getCurrentBulletSpeed();
  276. }
  277. const velocity = new Vec2(
  278. this.direction.x * this.speed,
  279. this.direction.y * this.speed
  280. );
  281. this.rigidBody.linearVelocity = velocity;
  282. console.log('✅ 子弹速度已应用:', velocity);
  283. }
  284. /**
  285. * 碰撞处理(整合原有逻辑)
  286. */
  287. private onHit(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
  288. const otherNode = otherCollider.node;
  289. console.log('💥 子弹碰撞:', otherNode.name);
  290. // 获取碰撞世界坐标
  291. let contactWorldPos: Vec3 = null;
  292. if (contact && (contact as any).getWorldManifold) {
  293. const wm = (contact as any).getWorldManifold();
  294. if (wm && wm.points && wm.points.length > 0) {
  295. contactWorldPos = new Vec3(wm.points[0].x, wm.points[0].y, 0);
  296. }
  297. }
  298. if (!contactWorldPos) {
  299. contactWorldPos = otherNode.worldPosition.clone();
  300. }
  301. // 检查是否击中敌人
  302. if (this.isEnemyNode(otherNode)) {
  303. console.log('🎯 击中敌人:', otherNode.name);
  304. // 对敌人造成伤害
  305. this.damageEnemy(otherNode);
  306. // 播放击中特效
  307. this.spawnHitEffect(contactWorldPos);
  308. // 根据子弹行为处理
  309. this.handleBehaviorOnHit(otherNode, contactWorldPos);
  310. } else if (otherNode.name.toLowerCase().includes('wall')) {
  311. console.log('🧱 击中墙体,销毁子弹');
  312. this.node.destroy();
  313. }
  314. }
  315. /**
  316. * 根据子弹行为处理击中逻辑
  317. */
  318. private handleBehaviorOnHit(enemyNode: Node, contactPos: Vec3) {
  319. switch (this.behavior) {
  320. case 'explosive':
  321. this.node.destroy();
  322. break;
  323. case 'piercing':
  324. // 继续飞行,不销毁
  325. break;
  326. case 'ricochet':
  327. if (this.ricochetLeft > 0) {
  328. this.ricochetLeft--;
  329. this.bounceDirection();
  330. } else {
  331. this.node.destroy();
  332. }
  333. break;
  334. case 'boomerang':
  335. // 命中后照样返程,由 boomerangTimer 处理,不销毁
  336. break;
  337. case 'area_burn':
  338. // 在地面生成灼烧特效
  339. this.spawnEffect(this.trailEffectPath, contactPos, true);
  340. this.scheduleOnce(() => {/* burn area ends */}, this.burnDuration);
  341. this.node.destroy();
  342. break;
  343. case 'homing':
  344. this.node.destroy();
  345. break;
  346. }
  347. }
  348. /**
  349. * 判断是否为敌人节点(从 BulletController 迁移)
  350. */
  351. private isEnemyNode(node: Node): boolean {
  352. const name = node.name.toLowerCase();
  353. return name.includes('enemy') ||
  354. name.includes('敌人') ||
  355. node.getComponent('EnemyInstance') !== null ||
  356. node.getComponent('EnemyComponent') !== null;
  357. }
  358. /**
  359. * 对敌人造成伤害(从 BulletController 迁移)
  360. */
  361. private damageEnemy(enemyNode: Node) {
  362. console.log('⚔️ 对敌人造成伤害:', this.damage);
  363. // 尝试调用敌人的受伤方法
  364. const enemyInstance = enemyNode.getComponent('EnemyInstance') as any;
  365. if (enemyInstance) {
  366. if (typeof enemyInstance.takeDamage === 'function') {
  367. enemyInstance.takeDamage(this.damage);
  368. return;
  369. }
  370. if (typeof enemyInstance.health === 'number') {
  371. enemyInstance.health -= this.damage;
  372. if (enemyInstance.health <= 0) {
  373. enemyNode.destroy();
  374. }
  375. return;
  376. }
  377. }
  378. // 备用方案:通过EnemyController
  379. const enemyController = find('Canvas/GameLevelUI/EnemyController')?.getComponent('EnemyController') as any;
  380. if (enemyController && typeof enemyController.damageEnemy === 'function') {
  381. enemyController.damageEnemy(enemyNode, this.damage);
  382. }
  383. }
  384. update(dt: number) {
  385. // 只有在初始化完成后才进行更新
  386. if (!this.isInitialized) return;
  387. // 生命周期管理
  388. this.updateLifetime(dt);
  389. // 检查是否飞出游戏区域
  390. this.checkOutOfBounds();
  391. // 子弹行为更新
  392. this.updateBehavior(dt);
  393. }
  394. /**
  395. * 更新生命周期
  396. */
  397. private updateLifetime(dt: number) {
  398. if (this.behavior === 'piercing' || this.behavior === 'boomerang') {
  399. this.lifeTimer -= dt;
  400. if (this.lifeTimer <= 0) {
  401. this.node.destroy();
  402. return;
  403. }
  404. }
  405. }
  406. /**
  407. * 检查越界(从 BulletController 迁移)
  408. */
  409. private checkOutOfBounds() {
  410. const canvas = find('Canvas');
  411. if (!canvas || !this.node || !this.node.isValid) return;
  412. const uiTransform = canvas.getComponent(UITransform);
  413. if (!uiTransform) return;
  414. const halfWidth = uiTransform.width / 2;
  415. const halfHeight = uiTransform.height / 2;
  416. const center = canvas.worldPosition;
  417. const left = center.x - halfWidth;
  418. const right = center.x + halfWidth;
  419. const bottom = center.y - halfHeight;
  420. const top = center.y + halfHeight;
  421. // 允许子弹稍微超出边界后再销毁,避免边缘误差
  422. const margin = 100;
  423. const pos = this.node.worldPosition;
  424. if (pos.x < left - margin || pos.x > right + margin ||
  425. pos.y < bottom - margin || pos.y > top + margin) {
  426. this.node.destroy();
  427. }
  428. }
  429. /**
  430. * 更新子弹行为
  431. */
  432. private updateBehavior(dt: number) {
  433. // boomerang返回
  434. if (this.behavior === 'boomerang') {
  435. this.boomerangTimer += dt;
  436. if (this.boomerangTimer >= this.boomerangReturnDelay) {
  437. this.direction.multiplyScalar(-1);
  438. if (this.rigidBody) {
  439. this.rigidBody.linearVelocity = new Vec2(
  440. this.direction.x * this.speed,
  441. this.direction.y * this.speed
  442. );
  443. }
  444. this.behavior = 'piercing'; // 之后按穿透逻辑处理
  445. }
  446. }
  447. // homing 导弹
  448. if (this.behavior === 'homing') {
  449. if (this.homingTimer > 0) {
  450. this.homingTimer -= dt;
  451. } else {
  452. const enemyContainer = find('Canvas/GameLevelUI/enemyContainer');
  453. if (enemyContainer && enemyContainer.children.length > 0) {
  454. let nearest: Node = null;
  455. let dist = 1e9;
  456. for (const e of enemyContainer.children) {
  457. if (!e.active) continue;
  458. const d = Vec3.distance(this.node.worldPosition, e.worldPosition);
  459. if (d < dist) {
  460. dist = d;
  461. nearest = e;
  462. }
  463. }
  464. if (nearest) {
  465. const dir = nearest.worldPosition.clone().subtract(this.node.worldPosition).normalize();
  466. // 线性插值方向
  467. this.direction.lerp(dir, this.homingStrength * dt);
  468. if (this.rigidBody) {
  469. this.rigidBody.linearVelocity = new Vec2(
  470. this.direction.x * this.speed,
  471. this.direction.y * this.speed
  472. );
  473. }
  474. }
  475. }
  476. }
  477. }
  478. }
  479. /**
  480. * 添加拖尾特效
  481. */
  482. private attachTrail() {
  483. this.spawnEffect(this.trailEffectPath, new Vec3(), true, this.node);
  484. }
  485. /**
  486. * 生成特效(整合并增强)
  487. */
  488. private spawnEffect(path: string, worldPos: Vec3, loop = false, parent?: Node) {
  489. if (!path) return;
  490. const match = path.match(/tx\/(tx\d{4})/);
  491. const code = match ? match[1] : '';
  492. const skeletonPath = code ? `Animation/WeaponTx/${code}/${code}` : path;
  493. console.log('✨ 尝试加载特效:', skeletonPath);
  494. resources.load(skeletonPath, sp.SkeletonData, (err, skData: sp.SkeletonData) => {
  495. if (err) {
  496. console.warn('加载特效失败:', skeletonPath, err);
  497. return;
  498. }
  499. const effectNode = new Node('Effect');
  500. const skeleton: sp.Skeleton = effectNode.addComponent(sp.Skeleton);
  501. skeleton.skeletonData = skData;
  502. skeleton.setAnimation(0, 'animation', loop);
  503. const targetParent = parent || find('Canvas/GameLevelUI/GameArea') || find('Canvas');
  504. if (targetParent) {
  505. targetParent.addChild(effectNode);
  506. if (parent) {
  507. effectNode.setPosition(0, 0, 0);
  508. } else {
  509. const parentTrans = targetParent.getComponent(UITransform);
  510. if (parentTrans) {
  511. const localPos = parentTrans.convertToNodeSpaceAR(worldPos);
  512. effectNode.position = localPos;
  513. } else {
  514. effectNode.setWorldPosition(worldPos.clone());
  515. }
  516. }
  517. }
  518. if (!loop) {
  519. skeleton.setCompleteListener(() => {
  520. effectNode.destroy();
  521. });
  522. }
  523. });
  524. }
  525. /**
  526. * 生成击中特效(从 BulletController 迁移)
  527. */
  528. private spawnHitEffect(worldPos: Vec3) {
  529. this.spawnEffect(this.hitEffectPath, worldPos, false);
  530. }
  531. /**
  532. * 弹射方向计算
  533. */
  534. private bounceDirection() {
  535. // 简单随机旋转 ricochetAngle
  536. const sign = Math.random() < 0.5 ? -1 : 1;
  537. const rad = this.ricochetAngle * (Math.PI / 180) * sign;
  538. const cos = Math.cos(rad), sin = Math.sin(rad);
  539. const nx = this.direction.x * cos - this.direction.y * sin;
  540. const ny = this.direction.x * sin + this.direction.y * cos;
  541. this.direction.set(nx, ny, 0);
  542. if (this.rigidBody) {
  543. this.rigidBody.linearVelocity = new Vec2(
  544. this.direction.x * this.speed,
  545. this.direction.y * this.speed
  546. );
  547. }
  548. }
  549. }