import { _decorator, BoxCollider2D, Collider2D, Component, EventTouch, find, Graphics, Node, PolygonCollider2D, Prefab, v3, Vec3, UITransform, Color, instantiate, sys, PhysicsSystem2D, ERaycast2DType, Vec2, Sprite } from 'cc'; import { UIBase } from 'db://assets/mx/module/ui/UIBase'; import { tantantanzidan } from './tantantanzidan'; import { UIID } from '../../game/ConfigRes'; import { GEvent } from 'db://assets/mx/module/event/GEvent'; import { jige } from './jige'; const { ccclass, property } = _decorator; @ccclass('tantantan') export class tantantan extends Component { @property(Prefab) zidanPrefab: Prefab = null; ui_center: Node = null; jigeNode: Node = null; jigeDieNode: Node = null; peopleDie: Node = null; peopleNode: Node = null; peopleHand1: Node = null; peopleHand2: Node = null; zidanView: Node = null; zidanPosiNode: Node = null; aimLineNode: Node = null; aimLineG: Graphics = null; isTouchStart: boolean = false; startTouchPos: Vec3 = v3(0, 0, 0); zidanNode: Node = null; isZidanFly: boolean = false; //子弹飞行中 flySpeed: number = 3000; flyAngle: number = 0; flyDir: Vec3 = v3(0, 1, 0); flyTime: number = 0; flyInterval: number = 0.03; outOfBoundsPadding: number = 200; raycastReflectPush: number = 8; private _wallColliders: Collider2D[] = []; isGameOver: boolean = false; curZidanIndex: number = 0; maxZidanIndex: number = 4; onLoad(): void { this.ui_center = find('ui_center', this.node); this.jigeNode = find('ui_center/鸡哥', this.node); this.jigeDieNode = find('ui_center/鸡哥死', this.node); this.peopleDie = find('ui_center/人物死', this.node); this.peopleNode = find('ui_center/人物', this.node); this.peopleHand1 = find('ui_center/人物/枪+手', this.node); this.peopleHand2 = find('ui_center/人物/后手', this.node); this.zidanPosiNode = find('ui_center/人物/枪+手/zidanPosi', this.node); this.zidanView = find('ui_center/zidanView', this.node); } protected onDisable(): void { this.ui_center.off(Node.EventType.TOUCH_START, this.onTouchStart, this); this.ui_center.off(Node.EventType.TOUCH_MOVE, this.onTouchMove, this); this.ui_center.off(Node.EventType.TOUCH_END, this.onTouchEnd, this); this.ui_center.off(Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this); GEvent.Ins.off(GEvent.ExitAtTrafficGame, this.closeView, this); GEvent.Ins.off(GEvent.RestartAtTrafficGame, this.restartGame, this); } start() { this.onInit(); } onInit(): void { // 瞄准线(红色圆点虚线) this.aimLineNode = new Node('aimLine'); this.node.addChild(this.aimLineNode); const trans = this.aimLineNode.addComponent(UITransform); // 覆盖整个 UI(使用锚点中心,便于 convertToNodeSpaceAR) trans.setContentSize(750, 1600); this.aimLineG = this.aimLineNode.addComponent(Graphics); this.aimLineG.lineWidth = 0; this.aimLineG.fillColor = new Color(255, 0, 0, 255); this.aimLineNode.active = false; //添加触摸监听事件 this.ui_center.on(Node.EventType.TOUCH_START, this.onTouchStart, this); this.ui_center.on(Node.EventType.TOUCH_MOVE, this.onTouchMove, this); this.ui_center.on(Node.EventType.TOUCH_END, this.onTouchEnd, this); this.ui_center.on(Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this); GEvent.Ins.on(GEvent.ExitAtTrafficGame, this.closeView, this); GEvent.Ins.on(GEvent.RestartAtTrafficGame, this.restartGame, this); // 缓存所有墙体 collider(tag==1),用于数学碰撞防穿透 this._wallColliders = this.ui_center.getComponentsInChildren(Collider2D).filter(c => c && c.tag === 1); } private _closestPointOnSegment2(p: Vec3, a: Vec3, b: Vec3): { cp: Vec3; t: number } { 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 { cp: a.clone(), t: 0 }; let t = (apx * abx + apy * aby) / abLen2; if (t < 0) t = 0; else if (t > 1) t = 1; return { cp: v3(a.x + abx * t, a.y + aby * t, 0), t }; } private _segmentIntersect(p: Vec3, p2: Vec3, q: Vec3, q2: Vec3): { hit: boolean; t: number; u: number; point: Vec3 } { // Solve p + t*(p2-p) = q + u*(q2-q) const rx = p2.x - p.x; const ry = p2.y - p.y; const sx = q2.x - q.x; const sy = q2.y - q.y; const denom = rx * sy - ry * sx; if (Math.abs(denom) < 0.000001) return { hit: false, t: 0, u: 0, point: v3() }; const qpx = q.x - p.x; const qpy = q.y - p.y; const t = (qpx * sy - qpy * sx) / denom; const u = (qpx * ry - qpy * rx) / denom; if (t < 0 || t > 1 || u < 0 || u > 1) return { hit: false, t, u, point: v3() }; return { hit: true, t, u, point: v3(p.x + t * rx, p.y + t * ry, 0) }; } private _getPolygonWorldPoints(col: PolygonCollider2D): Vec3[] | null { const pts: Vec2[] = (col as any).points; if (!pts || pts.length < 2) return null; const wm = col.node.worldMatrix; const out: Vec3[] = []; for (let i = 0; i < pts.length; i++) { const v = v3(pts[i].x, pts[i].y, 0); Vec3.transformMat4(v, v, wm); out.push(v); } return out; } private _getBoxWorldPoints(col: BoxCollider2D): Vec3[] { const size = col.size; const off = col.offset; const hw = size.width * 0.5; const hh = size.height * 0.5; const local = [ v3(off.x - hw, off.y - hh, 0), v3(off.x + hw, off.y - hh, 0), v3(off.x + hw, off.y + hh, 0), v3(off.x - hw, off.y + hh, 0), ]; const wm = col.node.worldMatrix; for (let i = 0; i < local.length; i++) { Vec3.transformMat4(local[i], local[i], wm); } return local; } /** 数学防穿透:线段 wp->nextWp 与墙体(tag=1)求最近交点 + 法线 */ private _manualWallHit(wp: Vec3, nextWp: Vec3): { point: Vec3; normal: Vec3 } | null { let bestT = Number.POSITIVE_INFINITY; let bestPoint: Vec3 | null = null; let bestNormal: Vec3 | null = null; for (const c of this._wallColliders) { if (!c || !c.enabledInHierarchy) continue; let polyPts: Vec3[] | null = null; if (c instanceof PolygonCollider2D) polyPts = this._getPolygonWorldPoints(c); else if (c instanceof BoxCollider2D) polyPts = this._getBoxWorldPoints(c); else continue; if (!polyPts || polyPts.length < 2) continue; for (let i = 0; i < polyPts.length; i++) { const a = polyPts[i]; const b = polyPts[(i + 1) % polyPts.length]; const inter = this._segmentIntersect(wp, nextWp, a, b); if (!inter.hit) continue; if (inter.t < bestT) { bestT = inter.t; bestPoint = inter.point; // 边法线(两种方向),选指向子弹的那个(hitPoint -> wp) const ex = b.x - a.x; const ey = b.y - a.y; let n1 = v3(-ey, ex, 0); let n2 = v3(ey, -ex, 0); if (n1.lengthSqr() > 0.000001) n1.normalize(); if (n2.lengthSqr() > 0.000001) n2.normalize(); const toPrev = v3(wp.x - inter.point.x, wp.y - inter.point.y, 0); const d1 = toPrev.x * n1.x + toPrev.y * n1.y; const d2 = toPrev.x * n2.x + toPrev.y * n2.y; bestNormal = d1 >= d2 ? n1 : n2; } } } if (!bestPoint || !bestNormal || bestT === Number.POSITIVE_INFINITY) return null; return { point: bestPoint, normal: bestNormal }; } onTouchStart(event: EventTouch) { if (this.isGameOver) { return; } console.log('onTouchStart'); if (this.isZidanFly) { return; } const touchPos = event.getUILocation(); this.startTouchPos = v3(touchPos.x, touchPos.y, 0); this.isTouchStart = true; this.updateAim(this.startTouchPos); } onTouchMove(event: EventTouch) { if (this.isGameOver) { return; } if (this.isZidanFly) { return; } console.log('onTouchMove'); const currentPos = event.getUILocation(); this.updateAim(v3(currentPos.x, currentPos.y, 0)); } onTouchEnd(event: EventTouch) { if (this.isGameOver) { return; } if (this.isZidanFly) { return; } console.log('onTouchEnd'); this.isTouchStart = false; if (this.aimLineNode) this.aimLineNode.active = false; //沿着角度发射子弹,子弹的初始角度是0,沿着y轴的正方向的所以需要旋转一下子弹的角度 const aimAngle = this.flyAngle; // updateAim 已写入,0°=+X const rad = (aimAngle * Math.PI) / 180; this.flyDir = v3(Math.cos(rad), Math.sin(rad), 0); // 子弹贴图 0°=+Y,所以需要 -90° 校正,让子弹头对齐飞行方向 const bulletAngle = aimAngle - 90; this.flyTime = 0; this.createZidanNode(bulletAngle); } /** 更新瞄准角度与瞄准线(0°=+X) */ private updateAim(touchWorld: Vec3) { if (!this.peopleNode || !this.peopleHand1 || !this.peopleHand2 || !this.zidanPosiNode || !this.aimLineG) return; // 本项目把 getUILocation 当作 UI 世界坐标使用 const peopleWorld = this.peopleNode.worldPosition.clone(); const dx = touchWorld.x - peopleWorld.x; const dy = touchWorld.y - peopleWorld.y; const angle = (Math.atan2(dy, dx) * 180) / Math.PI; // 0°=+X this.flyAngle = angle; this.peopleHand1.angle = angle; this.peopleHand2.angle = angle; // 画红色圆点虚线:zidanPosiNode -> touchWorld const lineTrans = this.aimLineNode.getComponent(UITransform); if (!lineTrans) return; const startLocal = lineTrans.convertToNodeSpaceAR(this.zidanPosiNode.worldPosition); const endLocal = lineTrans.convertToNodeSpaceAR(touchWorld); const dir = endLocal.subtract(startLocal); dir.z = 0; const len = Math.sqrt(dir.x * dir.x + dir.y * dir.y); if (len < 1) { this.aimLineNode.active = false; return; } dir.x /= len; dir.y /= len; const dotRadius = 4; const step = 18; // 点间距 const count = Math.floor(len / step); this.aimLineNode.active = true; this.aimLineG.clear(); for (let i = 0; i <= count; i++) { const x = startLocal.x + dir.x * step * i; const y = startLocal.y + dir.y * step * i; this.aimLineG.circle(x, y, dotRadius); this.aimLineG.fill(); } } createZidanNode(angle: number) { if (this.isZidanFly) { return; } this.isZidanFly = true; if (this.zidanNode) { this.zidanNode.destroy(); this.zidanNode = null; } gg.audio.playEffect({ name: '彩虹飞弹发射', path: 'sound/武器音效/' }); this.curZidanIndex++ this.zidanNode = instantiate(this.zidanPrefab); this.zidanNode.getComponent(tantantanzidan).setData(this); this.node.addChild(this.zidanNode); this.zidanNode.worldPosition = this.zidanPosiNode.worldPosition; this.zidanNode.angle = angle; } protected update(dt: number): void { if (!this.isZidanFly) { //子弹飞行 return } if (this.zidanNode.isValid) { this.flyTime += dt; if (this.flyTime >= this.flyInterval) { this.flyTime = 0; // 使用世界坐标移动 + 射线检测,避免高速子弹穿透细边/拐角 const wp = this.zidanNode.worldPosition; const nextWp = v3( wp.x + this.flyDir.x * this.flySpeed * dt, wp.y + this.flyDir.y * this.flySpeed * dt, wp.z ); const hits = PhysicsSystem2D.instance.raycast(wp, nextWp, ERaycast2DType.Closest); if (hits && hits.length > 0) { const hit = hits[0] as any; const col = hit.collider as any; if (col && col.tag === 1 && hit.normal) { const n = v3(hit.normal.x, hit.normal.y, 0); if (n.lengthSqr() > 0.000001) n.normalize(); const d0 = this.flyDir.clone(); d0.z = 0; if (d0.lengthSqr() < 0.000001) d0.set(0, 1, 0); const d = d0.normalize(); const dot = d.x * n.x + d.y * n.y; if (dot < -0.0001) { const r = v3(d.x - 2 * dot * n.x, d.y - 2 * dot * n.y, 0); if (r.lengthSqr() > 0.000001) { r.normalize(); this.flyDir = r; const aimAngle = (Math.atan2(r.y, r.x) * 180) / Math.PI; this.zidanNode.angle = aimAngle - 90; // 0°=+Y } } this.zidanNode.setWorldPosition( hit.point.x + n.x * this.raycastReflectPush, hit.point.y + n.y * this.raycastReflectPush, wp.z ); } else { this.zidanNode.setWorldPosition(nextWp); } } else { // 物理射线未命中时,使用数学求交防穿透 const mh = this._manualWallHit(wp, nextWp); if (mh) { const n = mh.normal; const d0 = this.flyDir.clone(); d0.z = 0; if (d0.lengthSqr() < 0.000001) d0.set(0, 1, 0); const d = d0.normalize(); const dot = d.x * n.x + d.y * n.y; if (dot < -0.0001) { const r = v3(d.x - 2 * dot * n.x, d.y - 2 * dot * n.y, 0); if (r.lengthSqr() > 0.000001) { r.normalize(); this.flyDir = r; const aimAngle = (Math.atan2(r.y, r.x) * 180) / Math.PI; this.zidanNode.angle = aimAngle - 90; } } this.zidanNode.setWorldPosition( mh.point.x + n.x * this.raycastReflectPush, mh.point.y + n.y * this.raycastReflectPush, wp.z ); } else { this.zidanNode.setWorldPosition(nextWp); } } //如果子弹超出屏幕,则销毁并且通知进入下一轮子弹游戏 屏幕大小需要获取当前屏幕的大小 const centerTrans = this.ui_center?.getComponent(UITransform) ?? null; if (!centerTrans) return; const wp2 = this.zidanNode.worldPosition; const lp = centerTrans.convertToNodeSpaceAR(wp2); const halfW = centerTrans.contentSize.width * 0.5; const halfH = centerTrans.contentSize.height * 0.5; const padding = this.outOfBoundsPadding; if (Math.abs(lp.x) > halfW + padding || Math.abs(lp.y) > halfH + padding) { console.log('子弹超出屏幕'); this.isZidanFly = false; if (this.zidanNode && this.zidanNode.isValid) { this.zidanNode.destroy(); } this.zidanNode = null; this.nextGame(); return; } } } } onTouchCancel(event: EventTouch) { console.log('onTouchCancel'); } closeView() { this.node.destroy(); } restartGame() { this.node.emit("_onMiniGameRestart"); this.closeView(); } successView() { if (this.isGameOver) { return; } this.isGameOver = true; this.isZidanFly = false this.jigeDieNode.active = true; this.jigeNode.active = false; this.node.emit("_onMiniGameEnd", true); } nextGame() { this.zidanNode = null; this.isZidanFly = false if (this.curZidanIndex >= this.maxZidanIndex) { this.failView() } else { this.jigeNode?.getComponent(jige)?.playHappyAnim(); // //人物红一下,1秒恢复 this.peopleNode.children.forEach(child => { child.getComponent(Sprite).color = Color.RED }); this.scheduleOnce(() => { this.peopleNode.children.forEach(child => { child.getComponent(Sprite).color = Color.WHITE }); }, 0.3); for (let i = 0; i < this.zidanView.children.length; i++) { let node = this.zidanView.children[i]; if (i <= this.curZidanIndex - 1) { node.getChildByName('zidanL').active = false; node.getChildByName('zidanB').active = true; } else { node.getChildByName('zidanL').active = true; node.getChildByName('zidanB').active = false; } } } } failView() { if (this.isGameOver) { return; } this.isGameOver = true; for (let i = 0; i < this.zidanView.children.length; i++) { let node = this.zidanView.children[i]; node.getChildByName('zidanL').active = false; node.getChildByName('zidanB').active = true; } this.isZidanFly = false this.peopleDie.active = true; this.peopleNode.active = false; this.node.emit("_onMiniGameEnd", false); } }