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.
506 lines
18 KiB
506 lines
18 KiB
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);
|
|
}
|
|
}
|
|
|
|
|
|
|