BulletController.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. import { _decorator, Component, Node, Vec2, Vec3, find, RigidBody2D, Collider2D, Contact2DType, IPhysics2DContact, instantiate, Prefab, UITransform, resources, sp } from 'cc';
  2. import { WeaponBlockExample } from './WeaponBlockExample';
  3. const { ccclass, property } = _decorator;
  4. /**
  5. * 子弹控制器 - 安全版本
  6. *
  7. * 设计原则:
  8. * 1. 在子弹添加到场景之前设置所有物理属性
  9. * 2. 延迟启用物理组件,避免m_world为null错误
  10. * 3. 确保物理组件在正确的时机激活
  11. */
  12. @ccclass('BulletController')
  13. export class BulletController extends Component {
  14. @property
  15. public speed: number = 300;
  16. @property
  17. public damage: number = 1;
  18. @property
  19. public lifetime: number = 5;
  20. @property({ tooltip: '击中特效路径(resources 相对路径,例如 Animation/tx/tx0001)' })
  21. public hitEffectPath: string = '';
  22. private targetEnemy: Node = null;
  23. private rigidBody: RigidBody2D = null;
  24. private lifeTimer: number = 0;
  25. private direction: Vec3 = null;
  26. private firePosition: Vec3 = null;
  27. private isInitialized: boolean = false;
  28. private needsPhysicsSetup: boolean = false;
  29. /**
  30. * 设置子弹的发射位置
  31. * @param position 发射位置(世界坐标)
  32. */
  33. public setFirePosition(position: Vec3) {
  34. this.firePosition = position.clone();
  35. this.needsPhysicsSetup = true;
  36. console.log('🎯 子弹发射位置已设置:', this.firePosition);
  37. }
  38. start() {
  39. console.log('🚀 子弹start()开始...');
  40. // 设置生命周期
  41. this.lifeTimer = this.lifetime;
  42. // 如果需要设置物理属性,延迟处理
  43. if (this.needsPhysicsSetup) {
  44. // 使用更短的延迟,确保物理世界准备好
  45. this.scheduleOnce(() => {
  46. this.setupPhysics();
  47. }, 0.05);
  48. }
  49. console.log('✅ 子弹start()完成');
  50. }
  51. /**
  52. * 设置物理属性
  53. */
  54. private setupPhysics() {
  55. console.log('🔧 开始设置子弹物理属性...');
  56. // 获取物理组件
  57. this.rigidBody = this.node.getComponent(RigidBody2D);
  58. const collider = this.node.getComponent(Collider2D);
  59. if (!this.rigidBody) {
  60. console.error('❌ 子弹预制体缺少RigidBody2D组件');
  61. return;
  62. }
  63. if (!collider) {
  64. console.error('❌ 子弹预制体缺少Collider2D组件');
  65. return;
  66. }
  67. console.log('✅ 物理组件获取成功');
  68. // 确保物理组件设置正确
  69. this.rigidBody.enabledContactListener = true;
  70. this.rigidBody.gravityScale = 0; // 不受重力影响
  71. this.rigidBody.linearDamping = 0; // 无阻尼
  72. this.rigidBody.angularDamping = 0; // 无角阻尼
  73. // 绑定碰撞事件
  74. collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
  75. console.log('✅ 碰撞事件已绑定');
  76. // 设置子弹位置
  77. if (this.firePosition) {
  78. this.setPositionInGameArea();
  79. }
  80. // 初始化方向和速度
  81. this.initializeDirection();
  82. this.isInitialized = true;
  83. console.log('✅ 子弹物理属性设置完成');
  84. }
  85. /**
  86. * 在GameArea中设置子弹位置
  87. */
  88. private setPositionInGameArea() {
  89. const gameArea = find('Canvas/GameLevelUI/GameArea');
  90. if (gameArea) {
  91. const gameAreaTransform = gameArea.getComponent(UITransform);
  92. if (gameAreaTransform) {
  93. const localPos = gameAreaTransform.convertToNodeSpaceAR(this.firePosition);
  94. this.node.position = localPos;
  95. console.log('✅ 子弹位置已设置:', localPos);
  96. }
  97. }
  98. }
  99. /**
  100. * 初始化子弹方向
  101. */
  102. private initializeDirection() {
  103. console.log('🎯 开始初始化子弹方向...');
  104. // 寻找最近的敌人
  105. this.findNearestEnemy();
  106. // 设置方向
  107. if (this.targetEnemy) {
  108. this.setDirectionToTarget();
  109. } else {
  110. // 随机方向
  111. const randomAngle = Math.random() * Math.PI * 2;
  112. this.direction = new Vec3(Math.cos(randomAngle), Math.sin(randomAngle), 0);
  113. console.log('🎲 使用随机方向');
  114. }
  115. // 应用速度
  116. this.applyVelocity();
  117. }
  118. // 寻找最近的敌人
  119. private findNearestEnemy() {
  120. const enemyContainer = find('Canvas/GameLevelUI/enemyContainer');
  121. if (!enemyContainer) {
  122. console.log('❌ 未找到敌人容器');
  123. return;
  124. }
  125. const enemies = enemyContainer.children.filter(child =>
  126. child.active &&
  127. (child.name.toLowerCase().includes('enemy') ||
  128. child.getComponent('EnemyInstance') !== null)
  129. );
  130. if (enemies.length === 0) {
  131. console.log('❌ 没有找到敌人');
  132. return;
  133. }
  134. let nearestEnemy: Node = null;
  135. let nearestDistance = Infinity;
  136. const bulletPos = this.node.worldPosition;
  137. for (const enemy of enemies) {
  138. const distance = Vec3.distance(bulletPos, enemy.worldPosition);
  139. if (distance < nearestDistance) {
  140. nearestDistance = distance;
  141. nearestEnemy = enemy;
  142. }
  143. }
  144. if (nearestEnemy) {
  145. this.targetEnemy = nearestEnemy;
  146. console.log(`🎯 锁定目标: ${nearestEnemy.name}`);
  147. }
  148. }
  149. // 设置朝向目标的方向
  150. private setDirectionToTarget() {
  151. if (!this.targetEnemy) return;
  152. const targetPos = this.targetEnemy.worldPosition;
  153. const currentPos = this.node.worldPosition;
  154. this.direction = targetPos.clone().subtract(currentPos).normalize();
  155. console.log('🎯 方向已设置,朝向目标敌人');
  156. }
  157. // 应用速度
  158. private applyVelocity() {
  159. if (!this.rigidBody || !this.direction) {
  160. console.error('❌ 无法应用速度:缺少刚体或方向');
  161. return;
  162. }
  163. // 覆盖速度值(全局控制)
  164. const weaponGlobal = WeaponBlockExample.getInstance && WeaponBlockExample.getInstance();
  165. if (weaponGlobal) {
  166. this.speed = weaponGlobal.getCurrentBulletSpeed();
  167. }
  168. const velocity = new Vec2(
  169. this.direction.x * this.speed,
  170. this.direction.y * this.speed
  171. );
  172. this.rigidBody.linearVelocity = velocity;
  173. console.log('✅ 子弹速度已应用:', velocity);
  174. }
  175. // 碰撞检测
  176. onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
  177. const otherNode = otherCollider.node;
  178. console.log('💥 子弹碰撞:', otherNode.name);
  179. let contactWorldPos: Vec3 = null;
  180. if (contact && (contact as any).getWorldManifold) {
  181. const wm = (contact as any).getWorldManifold();
  182. if (wm && wm.points && wm.points.length > 0) {
  183. contactWorldPos = new Vec3(wm.points[0].x, wm.points[0].y, 0);
  184. }
  185. }
  186. if (!contactWorldPos) {
  187. contactWorldPos = otherNode.worldPosition.clone();
  188. }
  189. // 检查是否击中敌人
  190. if (this.isEnemyNode(otherNode)) {
  191. console.log('🎯 击中敌人:', otherNode.name);
  192. // 播放击中特效
  193. this.spawnHitEffect(contactWorldPos);
  194. this.damageEnemy(otherNode);
  195. this.node.destroy();
  196. } else if (otherNode.name.toLowerCase().includes('wall')) {
  197. console.log('🧱 击中墙体,销毁子弹');
  198. this.node.destroy();
  199. }
  200. }
  201. // 判断是否为敌人节点
  202. private isEnemyNode(node: Node): boolean {
  203. const name = node.name.toLowerCase();
  204. return name.includes('enemy') ||
  205. name.includes('敌人') ||
  206. node.getComponent('EnemyInstance') !== null ||
  207. node.getComponent('EnemyComponent') !== null;
  208. }
  209. // 对敌人造成伤害
  210. private damageEnemy(enemyNode: Node) {
  211. console.log('⚔️ 对敌人造成伤害:', this.damage);
  212. // 尝试调用敌人的受伤方法
  213. const enemyInstance = enemyNode.getComponent('EnemyInstance') as any;
  214. if (enemyInstance) {
  215. if (typeof enemyInstance.takeDamage === 'function') {
  216. enemyInstance.takeDamage(this.damage);
  217. return;
  218. }
  219. if (typeof enemyInstance.health === 'number') {
  220. enemyInstance.health -= this.damage;
  221. if (enemyInstance.health <= 0) {
  222. enemyNode.destroy();
  223. }
  224. return;
  225. }
  226. }
  227. // 备用方案:通过EnemyController
  228. const enemyController = find('Canvas/GameLevelUI/EnemyController')?.getComponent('EnemyController') as any;
  229. if (enemyController && typeof enemyController.damageEnemy === 'function') {
  230. enemyController.damageEnemy(enemyNode, this.damage);
  231. }
  232. }
  233. update(dt: number) {
  234. // 只有在初始化完成后才进行更新
  235. if (!this.isInitialized) return;
  236. // 生命周期倒计时
  237. this.lifeTimer -= dt;
  238. if (this.lifeTimer <= 0) {
  239. this.node.destroy();
  240. }
  241. // 检查是否飞出游戏区域
  242. this.checkOutOfBounds();
  243. }
  244. /**
  245. * 如果子弹飞出 GameArea 边界(留有一定安全距离)则自动销毁
  246. */
  247. private checkOutOfBounds() {
  248. // 使用整个屏幕(Canvas)范围判定,而不是 GameArea
  249. const canvas = find('Canvas');
  250. if (!canvas || !this.node || !this.node.isValid) return;
  251. const uiTransform = canvas.getComponent(UITransform);
  252. if (!uiTransform) return;
  253. const halfWidth = uiTransform.width / 2;
  254. const halfHeight = uiTransform.height / 2;
  255. const center = canvas.worldPosition;
  256. const left = center.x - halfWidth;
  257. const right = center.x + halfWidth;
  258. const bottom = center.y - halfHeight;
  259. const top = center.y + halfHeight;
  260. // 允许子弹稍微超出边界后再销毁,避免边缘误差
  261. const margin = 100;
  262. const pos = this.node.worldPosition;
  263. if (pos.x < left - margin || pos.x > right + margin || pos.y < bottom - margin || pos.y > top + margin) {
  264. this.node.destroy();
  265. }
  266. }
  267. public setEffectPaths(hit: string | null, trail?: string | null) {
  268. if (hit) this.hitEffectPath = hit;
  269. // 目前 trail 未实现
  270. }
  271. private spawnHitEffect(worldPos: Vec3) {
  272. if (!this.hitEffectPath) return;
  273. // 将 Animation/tx/tx0002 转成 Animation/WeaponTx/tx0002/tx0002
  274. let skeletonPath = '';
  275. const match = this.hitEffectPath.match(/tx\/(tx\d{4})/);
  276. if (match && match[1]) {
  277. const code = match[1];
  278. skeletonPath = `Animation/WeaponTx/${code}/${code}`;
  279. } else {
  280. console.warn('无法解析击中特效路径,直接使用原路径尝试加载:', this.hitEffectPath);
  281. skeletonPath = this.hitEffectPath;
  282. }
  283. console.log('✨ 尝试加载击中特效:', skeletonPath);
  284. resources.load(skeletonPath, sp.SkeletonData, (err, skData: sp.SkeletonData) => {
  285. if (err) {
  286. console.warn('加载击中特效失败:', skeletonPath, err);
  287. return;
  288. }
  289. const effectNode = new Node('HitEffect');
  290. const skeleton: sp.Skeleton = effectNode.addComponent(sp.Skeleton);
  291. skeleton.skeletonData = skData;
  292. skeleton.setAnimation(0, 'animation', false);
  293. // 将效果节点添加到GameArea(或Canvas)
  294. const gameArea = find('Canvas/GameLevelUI/GameArea') || find('Canvas');
  295. if (gameArea) {
  296. gameArea.addChild(effectNode);
  297. // 转换坐标
  298. const parentTrans = gameArea.getComponent(UITransform);
  299. if (parentTrans) {
  300. const localPos = parentTrans.convertToNodeSpaceAR(worldPos);
  301. effectNode.position = localPos;
  302. } else {
  303. effectNode.setWorldPosition(worldPos.clone());
  304. }
  305. } else {
  306. effectNode.setWorldPosition(worldPos.clone());
  307. }
  308. // 播放完毕后销毁
  309. skeleton.setCompleteListener(() => {
  310. effectNode.destroy();
  311. });
  312. });
  313. }
  314. }