import { _decorator, Collider2D, Component, Contact2DType, director, IPhysics2DContact, PolygonCollider2D, v3, Vec2, Vec3 } from 'cc'; import { tantantan } from './tantantan'; const { ccclass, property } = _decorator; @ccclass('tantantanzidan') export class tantantanzidan extends Component { @property({ tooltip: '反射调试打印(复现问题时打开)' }) debugReflect: boolean = false; isdie:boolean = false; tantantanObj:tantantan = null; private _lastReflectFrame = -999999; private _lastReflectNormal: Vec3 = v3(0, 0, 0); private _reflectCooldownUntilFrame = -999999; private _closestPointOnSegment(p: Vec3, a: Vec3, b: Vec3): Vec3 { const abx = b.x - a.x; const aby = b.y - a.y; const apx = p.x - a.x; const apy = p.y - a.y; const abLen2 = abx * abx + aby * aby; if (abLen2 < 0.000001) return a.clone(); let t = (apx * abx + apy * aby) / abLen2; if (t < 0) t = 0; else if (t > 1) t = 1; return v3(a.x + abx * t, a.y + aby * t, 0); } /** contact 为空时:用多边形最近边求法线与接触点(世界坐标) */ private _estimateNormalFromPolygon(otherCollider: Collider2D, bulletWorld: Vec3): { n: Vec3; cp: Vec3 } | null { const poly = otherCollider as any as PolygonCollider2D; if (!poly || !Array.isArray((poly as any).points) || (poly as any).points.length < 2) return null; const ptsLocal: Vec2[] = (poly as any).points; const wm = otherCollider.node.worldMatrix; const ptsWorld: Vec3[] = ptsLocal.map((p) => { const v = v3(p.x, p.y, 0); Vec3.transformMat4(v, v, wm); return v; }); let bestDist2 = Number.POSITIVE_INFINITY; let bestA: Vec3 | null = null; let bestB: Vec3 | null = null; let bestCP: Vec3 | null = null; for (let i = 0; i < ptsWorld.length; i++) { const a = ptsWorld[i]; const b = ptsWorld[(i + 1) % ptsWorld.length]; const cp = this._closestPointOnSegment(bulletWorld, a, b); const dx = bulletWorld.x - cp.x; const dy = bulletWorld.y - cp.y; const dist2 = dx * dx + dy * dy; if (dist2 < bestDist2) { bestDist2 = dist2; bestA = a; bestB = b; bestCP = cp; } } if (!bestA || !bestB || !bestCP) return null; // 边向量 const sx = bestB.x - bestA.x; const sy = bestB.y - bestA.y; if (sx * sx + sy * sy < 0.000001) return null; // 两个法线候选 const n1 = v3(-sy, sx, 0); const n2 = v3(sy, -sx, 0); n1.normalize(); n2.normalize(); // 选指向子弹的那个(cp -> bullet) const toB = v3(bulletWorld.x - bestCP.x, bulletWorld.y - bestCP.y, 0); if (toB.lengthSqr() < 0.000001) return { n: n1, cp: bestCP }; const d1 = toB.x * n1.x + toB.y * n1.y; const d2 = toB.x * n2.x + toB.y * n2.y; return { n: (d1 >= d2 ? n1 : n2), cp: bestCP }; } private _reflectIfNeeded(otherCollider: Collider2D, contact?: IPhysics2DContact | null) { if (!this.tantantanObj) return; const frame = director.getTotalFrames(); if (frame < this._reflectCooldownUntilFrame) return; // 入射方向(世界) const d0 = this.tantantanObj.flyDir?.clone() ?? v3(0, 1, 0); d0.z = 0; if (d0.lengthSqr() < 0.000001) d0.set(0, 1, 0); const d = d0.normalize(); const bwp = this.node.worldPosition; // 法线/接触点(世界):优先用 manifold;没有就用多边形估算 let n: Vec3 | null = null; let cpWorld: Vec3 | null = null; const manifold = contact?.getWorldManifold?.(); const nn: Vec2 | undefined = manifold ? (manifold as any)?.normal : undefined; const pts: Vec2[] | undefined = manifold ? (manifold as any)?.points : undefined; if (nn) { n = v3(nn.x, nn.y, 0); if (n.lengthSqr() > 0.000001) n.normalize(); } if (pts && pts.length > 0) { cpWorld = v3(pts[0].x, pts[0].y, 0); } if (!n || n.lengthSqr() < 0.000001 || !cpWorld) { const est = this._estimateNormalFromPolygon(otherCollider, bwp); if (est) { n = est.n; cpWorld = est.cp; } } if (!n || !cpWorld) { if (this.debugReflect) { console.log('[reflect-skip] no contact/manifold; no polygon estimate', { frame, otherName: otherCollider.node?.name, otherTag: otherCollider.tag, }); } return; } // 矫正法线方向:确保 n 指向子弹(cp -> bullet) const toBullet = v3(bwp.x - cpWorld.x, bwp.y - cpWorld.y, 0); if (toBullet.lengthSqr() > 0.000001) { if (toBullet.x * n.x + toBullet.y * n.y < 0) { n.x = -n.x; n.y = -n.y; } } // 只有在“确实撞向墙面”时才反射:d·n < 0 const dot = d.x * n.x + d.y * n.y; if (dot >= -0.0001) { if (this.debugReflect) { console.log('[reflect-skip]', { frame, dot, d: { x: d.x, y: d.y }, n: { x: n.x, y: n.y }, bullet: { x: bwp.x, y: bwp.y }, contactPoint: { x: cpWorld.x, y: cpWorld.y }, otherName: otherCollider.node?.name, }); } return; } // 同帧/同法线去抖(多边形拐角可能同帧触发多次) if (frame === this._lastReflectFrame) { const ndot = this._lastReflectNormal.x * n.x + this._lastReflectNormal.y * n.y; if (ndot > 0.999) return; } this._lastReflectFrame = frame; this._lastReflectNormal.set(n.x, n.y, 0); // 标准反射:r = d - 2(d·n)n const r = v3(d.x - 2 * dot * n.x, d.y - 2 * dot * n.y, 0); if (r.lengthSqr() < 0.000001) return; r.normalize(); // 更新方向与角度(0°=+Y) this.tantantanObj.flyDir = r; const aimAngle = (Math.atan2(r.y, r.x) * 180) / Math.PI; this.node.angle = this._normalizeAngleDeg(aimAngle - 90); // 推出墙体,避免持续贴边导致每帧反射 const push = 8; const bwp2 = this.node.worldPosition; this.node.setWorldPosition(bwp2.x + n.x * push, bwp2.y + n.y * push, bwp2.z); // 反射后短冷却 1 帧,避免“贴边抖动”死循环 this._reflectCooldownUntilFrame = frame + 1; if (this.debugReflect) { console.log('[reflect]', { frame, dot, d: { x: d.x, y: d.y }, n: { x: n.x, y: n.y }, r: { x: r.x, y: r.y }, bullet: { x: bwp.x, y: bwp.y }, contactPoint: { x: cpWorld.x, y: cpWorld.y }, otherName: otherCollider.node?.name, }); } } private _normalizeAngleDeg(a: number): number { // normalize to [-180, 180] a = ((a % 360) + 360) % 360; if (a > 180) a -= 360; return a; } setData(tanObj:tantantan){ this.tantantanObj = tanObj; } //注册弹事件 protected onDisable(): void { let collider = this.getComponent(Collider2D); if (collider) { collider.off(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); } } protected onEnable(): void { let collider = this.getComponent(Collider2D); if (collider) { collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); } } onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact?: IPhysics2DContact | null){ //selfCollider 子弹 //otherCollider 怪物和door //怪物要区分人还是鸡哥 if(this.isdie){ return; } //console.log('onBeginContact', selfCollider, otherCollider); if(otherCollider.tag == 1){ // 碰撞到多边形包围盒,反射子弹(手动移动场景下 BEGIN_CONTACT 更可靠) this._reflectIfNeeded(otherCollider, contact); return; }else if(otherCollider.tag == 2){ //打到了鸡哥 //子弹移除 //this.tantantanObj.isZidanFly = false; this.isdie = true; // 物理回调里直接 destroy/切换刚体状态会报警告:延迟到下一帧执行 if (selfCollider) selfCollider.enabled = false; this.scheduleOnce(() => { if (!this.node?.isValid) return; //结算成功 this.tantantanObj?.successView(); this.node.destroy(); }, 0); }else if(otherCollider.tag == 3){ this.isdie = true; //打到了人物 if (selfCollider) selfCollider.enabled = false; this.scheduleOnce(() => { if (!this.node?.isValid) return; this.tantantanObj.isZidanFly = false; //子弹移除,继续下一发游戏 this.tantantanObj?.nextGame(); this.node.destroy(); }, 0); } } }