消消方块阵换皮表情
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

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);
}
}