You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
265 lines
9.4 KiB
265 lines
9.4 KiB
|
1 week ago
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|