12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856 |
- <template>
- <div class="game-container">
- <div id="game"></div>
- <canvas ref="canvasRef" :width="clientObj.width" :height="clientObj.height"
- style="position:fixed;left: 0; top: 0;"></canvas>
- </div>
- </template>
- <script setup name="Fruit" lang="ts">
- import { onMounted, ref } from 'vue';
- import Phaser from 'phaser';
- import { initSpeech, speckText, playMusic, controlMusic, speckCancel, chineseNumber } from '@/utils/speech';
- import { useWebSocket } from '@/utils/bodyposeWs';
- const { proxy } = getCurrentInstance() as any;
- const router = useRouter();
- const { bodyposeWs, startDevice, checkBodypose, openBodypose, terminateBodypose, suspendBodypose, resumeBodypose, getBodyposeState, closeWS } = useWebSocket();
- const gameRef = ref(null); // 用于保存游戏实例的ref
- const canvasRef = ref(null);
- const data = reactive<any>({
- bodyposeData: {},//姿态信息
- bodyposeState: false,//姿态识别窗口状态
- parameter: {},//参数
- deviceInfo: {},//设备信息
- againNum: 0,//再次启动次数
- againTimer: null,//定时状态
- wsState: false,//WS状态
- clientObj: {},//浏览器对象
- boxes: [],//四个点坐标
- proportion: null,//人框和屏幕比例
- scaleRatio: 2.2,//素材比例
- });
- const { bodyposeData, bodyposeState, parameter, deviceInfo, againNum, againTimer, wsState, clientObj, boxes, proportion, scaleRatio } = toRefs(data);
- /**
- * 初始化
- */
- const getInit = async () => {
- console.log("触发姿态识别")
- let deviceid = localStorage.getItem('deviceid') || '';
- if (!deviceid) {
- proxy?.$modal.msgError(`请重新登录绑定设备号后使用`);
- return false;
- }
- bodyposeState.value = true;
- if (wsState.value) {
- proxy?.$modal.msgWarning(`操作过快,请稍后重试`);
- setTimeout(() => {
- bodyposeState.value = false;
- }, 1000)
- return false;
- }
- speckText("正在姿态识别");
- proxy?.$modal.msgWarning(`正在姿态识别`);
- bodyposeWs((e: any) => {
- //console.log("bodyposeWS", e)
- if (e?.wksid) {
- wsState.value = true;
- //获取设备信息
- startDevice({ deviceid: deviceid });
- console.log("获取设备信息")
- }
- if (e?.type == 'fe_device_init_result') {
- //接收设备信息并发送请求
- if (e?.device_info) {
- deviceInfo.value = e.device_info;
- getCheckBodypose();
- console.log("返回设备信息,检查是否支持姿态识别")
- } else {
- proxy?.$modal.msgError(`设备信息缺失,请重新登录绑定设备号后使用`);
- }
- }
- if (e?.cmd == 'check_bodyposecontroller_available') {
- let handcontroller_id = deviceInfo.value.handcontroller_id;
- if (e?.code == 0) {
- //查看姿态识别状态,如果不处于关闭就先关闭再重新启动(可能会APP退出然后工作站还在运行的可能性)
- getBodyposeState(handcontroller_id);
- againNum.value = 0;
- againTimer.value = null;
- clearTimeout(againTimer.value);
- console.log("查看姿态识别状态")
- } else {
- //尝试多次查询姿态识别状态
- if (againNum.value <= 2) {
- againTimer.value = setTimeout(() => {
- getCheckBodypose();
- }, 500)
- againNum.value++;
- } else {
- let msg = "";
- if (e.code == 102402) {
- msg = `多次连接失败请重试,姿态识别模块被占用`;
- } else {
- msg = `多次连接失败请重试,姿态识别模块不可用,code:${e.code}`;
- }
- proxy?.$modal.msgWarning(msg);
- againNum.value = 0;
- againTimer.value = null;
- clearTimeout(againTimer.value);
- getCloseBodypose();//直接关闭
- }
- }
- }
- if (e?.cmd == 'get_bodyposecontroller_state') {
- let handcontroller_id = deviceInfo.value.handcontroller_id;
- //state说明: 0:关闭,3:空闲,36:工作中
- if ([3, 36].includes(e.state)) {
- getCloseBodypose();
- proxy?.$modal.msgWarning(`请重新姿态识别`);
- } else {
- openBodypose(handcontroller_id);
- }
- }
- if (e?.type == 'bodyposecontroller_result') {
- let arr = e.data.result.keypoints;
- let result = [];
- for (let i = 0; i < arr.length; i += 3) {
- result.push(arr.slice(i, i + 2));
- }
- // console.log("result", result)
- bodyposeData.value = result;
- if (boxes.value.length == 0) {
- speckText("识别成功");
- proxy?.$modal.msgWarning(`识别成功`);
- let arr = e.data.result.boxes;
- boxes.value = [{ x: arr[0], y: arr[3] }, { x: arr[0], y: arr[1] }, { x: arr[2], y: arr[1] }, { x: arr[2], y: arr[3] }]
- proportion.value = (clientObj.value.height / (arr[3] - arr[1])).toFixed(2);
- }
- getCanvas();
- }
- if (e?.cmd == 'terminate_bodyposecontroller') {
- if (e?.code == 0) {
- closeWS();
- bodyposeState.value = false;
- }
- }
- if (e?.type == 'disconnect') {
- wsState.value = false;
- }
- });
- };
- /**
- * 查询姿态识别状态
- */
- const getCheckBodypose = () => {
- let handcontroller_id = deviceInfo.value.handcontroller_id;
- //检查是否支持姿态识别
- checkBodypose(handcontroller_id);
- };
- /**
- * 关闭姿态识别
- */
- const getCloseBodypose = () => {
- let handcontroller_id = deviceInfo.value.handcontroller_id;
- terminateBodypose(handcontroller_id);
- bodyposeState.value = false;
- speckCancel(); //停止播报
- setTimeout(() => {
- if (wsState.value) {
- closeWS();
- }
- }, 3000)
- };
- /**
- * 绘图
- */
- const getCanvas = () => {
- const canvas: any = canvasRef.value;
- const ctx = canvas.getContext('2d');
- // 清空整个画布
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- // 保存当前状态
- ctx.save();
- function calculateOffset(a: any, b: any) {
- return {
- x: b.x - a.x,
- y: b.y - a.y
- };
- }
- const pointA = { x: clientObj.value.width / 2, y: clientObj.value.height / 2 };
- const pointB = { x: (boxes.value[2].x + boxes.value[0].x) / 2, y: (boxes.value[3].y + boxes.value[1].y) / 2 };
- const offset = calculateOffset(pointA, pointB);
- ctx.translate(-offset.x, -offset.y);
- // console.log("Canvas分辨率", clientObj.value);
- // console.log("人体图片四点坐标", boxes.value)
- // console.log("Canvas中心", pointA);
- // console.log("人体中心", pointB);
- // console.log("offset", offset)
- // console.log("proportion.value",proportion.value)
- const originalPoints = bodyposeData.value;
- // 计算缩放后坐标
- const postData = originalPoints.map((point: any) => {
- const newX = (point[0] - pointB.x) * proportion.value + pointB.x;
- const newY = (point[1] - pointB.y) * proportion.value + pointB.y;
- return [newX, newY];
- });
- // console.log("原始坐标:", originalPoints);
- // console.log("缩放后坐标:", postData);
- //externalMethod(postData[10][0] - offset.x, postData[10][1] - offset.y)
- externalRightHandMethod(postData[10][0] - offset.x, postData[10][1] - offset.y)
- externalLeftHandMethod(postData[9][0] - offset.x, postData[9][1] - offset.y)
- //绘制头部
- const point1 = { x: postData[4][0], y: postData[4][1] };
- const point2 = { x: postData[3][0], y: postData[3][1] };
- // 计算椭圆参数
- const centerX = (point1.x + point2.x) / 2; // 椭圆中心X
- const centerY = (point1.y + point2.y) / 2; // 椭圆中心Y
- const distance = Math.sqrt(
- Math.pow(point2.x - point1.x, 2) +
- Math.pow(point2.y - point1.y, 2)
- ); // 两个焦点之间的距离
- const radiusX = distance * 0.5; // 水平半径(可调整)
- const radiusY = distance * 0.6; // 垂直半径(可调整)
- // 1. 绘制填充椭圆
- ctx.beginPath();
- ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2);
- ctx.fillStyle = 'red'; // 填充颜色
- ctx.fill(); // 填充
- // 2. 绘制边框
- ctx.strokeStyle = 'red';
- ctx.lineWidth = 5;
- ctx.stroke();
- // 绘制每个点
- postData.forEach((point: any, index: number) => {
- //眼睛鼻子不显示
- if (![0, 1, 2].includes(index)) {
- const [x, y] = point;
- ctx.beginPath();
- ctx.arc(x, y, 5, 0, Math.PI * 2); // 绘制半径为5的圆点
- ctx.fillStyle = 'red';
- ctx.fill();
- ctx.lineWidth = 1;
- ctx.stroke();
- }
- });
- // 根据点关系连线
- const arr = [[10, 8], [8, 6], [6, 5], [5, 7], [7, 9], [6, 12], [5, 11], [12, 11], [12, 14], [14, 16], [11, 13], [13, 15]]
- arr.forEach((point: any) => {
- let index1 = point[0];
- let index2 = point[1];
- //连线
- const dian1 = { x: postData[index1][0], y: postData[index1][1] };
- const dian2 = { x: postData[index2][0], y: postData[index2][1] };
- // 绘制连线
- ctx.beginPath();
- ctx.moveTo(dian1.x, dian1.y); // 起点
- ctx.lineTo(dian2.x, dian2.y); // 终点
- ctx.strokeStyle = 'red'; // 线条颜色
- ctx.lineWidth = 3; // 线条宽度
- ctx.stroke(); // 描边
- });
- ctx.restore(); // 恢复状态
- };
- // 游戏容器和尺寸相关
- const width = document.documentElement.clientWidth;
- const height = document.documentElement.clientHeight;
- const wRatio = document.documentElement.clientWidth / 640;
- const hRatio = document.documentElement.clientHeight / 480;
- const gameContainer = ref(null);
- let game = null;
- // 工具类
- const mathTool = {
- init() {
- },
- // 计算延长线,p2往p1延长
- calcParallel(p1, p2, L) {
- const p = {};
- if (p1.x === p2.x) {
- if (p1.y - p2.y > 0) {
- p.x = p1.x;
- p.y = p1.y + L;
- } else {
- p.x = p1.x;
- p.y = p1.y - L;
- }
- } else {
- const k = (p2.y - p1.y) / (p2.x - p1.x);
- if (p2.x - p1.x < 0) {
- p.x = p1.x + L / Math.sqrt(1 + k * k);
- p.y = p1.y + L * k / Math.sqrt(1 + k * k);
- } else {
- p.x = p1.x - L / Math.sqrt(1 + k * k);
- p.y = p1.y - L * k / Math.sqrt(1 + k * k);
- }
- }
- p.x = Math.round(p.x);
- p.y = Math.round(p.y);
- return new Phaser.Math.Vector2(p.x, p.y);
- },
- // 计算垂直线,p2点开始垂直
- calcVertical(p1, p2, L, isLeft) {
- const p = {};
- if (p1.y === p2.y) {
- p.x = p2.x;
- if (isLeft) {
- if (p2.x - p1.x > 0) {
- p.y = p2.y - L;
- } else {
- p.y = p2.y + L;
- }
- } else {
- if (p2.x - p1.x > 0) {
- p.y = p2.y + L;
- } else {
- p.y = p2.y - L;
- }
- }
- } else {
- const k = -(p2.x - p1.x) / (p2.y - p1.y);
- if (isLeft) {
- if (p2.y - p1.y > 0) {
- p.x = p2.x + L / Math.sqrt(1 + k * k);
- p.y = p2.y + L * k / Math.sqrt(1 + k * k);
- } else {
- p.x = p2.x - L / Math.sqrt(1 + k * k);
- p.y = p2.y - L * k / Math.sqrt(1 + k * k);
- }
- } else {
- if (p2.y - p1.y > 0) {
- p.x = p2.x - L / Math.sqrt(1 + k * k);
- p.y = p2.y - L * k / Math.sqrt(1 + k * k);
- } else {
- p.x = p2.x + L / Math.sqrt(1 + k * k);
- p.y = p2.y + L * k / Math.sqrt(1 + k * k);
- }
- }
- }
- p.x = Math.round(p.x);
- p.y = Math.round(p.y);
- return new Phaser.Math.Vector2(p.x, p.y);
- },
- // 形成刀光点
- generateBlade(points) {
- const res = [];
- if (points.length <= 0) {
- return res;
- } else if (points.length === 1) {
- const oneLength = 6;
- res.push(new Phaser.Math.Vector2(points[0].x - oneLength, points[0].y));
- res.push(new Phaser.Math.Vector2(points[0].x, points[0].y - oneLength));
- res.push(new Phaser.Math.Vector2(points[0].x + oneLength, points[0].y));
- res.push(new Phaser.Math.Vector2(points[0].x, points[0].y + oneLength));
- } else {
- const tailLength = 10;
- const headLength = 20;
- const tailWidth = 1;
- const headWidth = 6;
- res.push(this.calcParallel(points[0], points[1], tailLength));
- for (let i = 0; i < points.length - 1; i++) {
- res.push(this.calcVertical(
- points[i + 1],
- points[i],
- Math.round((headWidth - tailWidth) * i / (points.length - 1) + tailWidth),
- true
- ));
- }
- res.push(this.calcVertical(
- points[points.length - 2],
- points[points.length - 1],
- headWidth,
- false
- ));
- res.push(this.calcParallel(
- points[points.length - 1],
- points[points.length - 2],
- headLength
- ));
- res.push(this.calcVertical(
- points[points.length - 2],
- points[points.length - 1],
- headWidth,
- true
- ));
- for (let i = points.length - 1; i > 0; i--) {
- res.push(this.calcVertical(
- points[i],
- points[i - 1],
- Math.round((headWidth - tailWidth) * (i - 1) / (points.length - 1) + tailWidth),
- false
- ));
- }
- }
- return res;
- },
- randomMinMax(min, max) {
- return Math.random() * (max - min) + min;
- },
- randomPosX() {
- return this.randomMinMax(-100, width + 100);
- },
- randomPosY() {
- //return this.randomMinMax(100, 200) + height;
- return this.randomMinMax(height - 100, height);
- },
- randomVelocityX(posX) {
- if (posX < 0) {
- return this.randomMinMax(100, 400);
- } else if (posX >= 0 && posX < width / 2) {
- return this.randomMinMax(0, 400);
- } else if (posX >= width / 2 && posX < width) {
- return this.randomMinMax(-400, 0);
- } else {
- return this.randomMinMax(-400, -100);
- }
- },
- randomVelocityY() {
- const myH = height - 600;
- // 调整范围为原速度的70%左右(根据需要微调)
- return this.randomMinMax(-630 - myH * 0.5, -595 - myH * 0.5);
- },
- degCos(deg) {
- return Math.cos(deg * Math.PI / 180);
- },
- degSin(deg) {
- return Math.sin(deg * Math.PI / 180);
- },
- shuffle(o) {
- for (let j, x, i = o.length; i; j = parseInt(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
- return o;
- }
- };
- // 炸弹类
- class Bomb {
- constructor(envConfig) {
- this.env = envConfig;
- this.game = envConfig.scene;
- this.sprite = null;
- this.bombImage = null;
- this.bombSmoke = null;
- this.bombEmit = null;
- this.init();
- }
- init() {
- // 创建容器
- this.sprite = this.game.add.container(
- this.env.x || 0,
- this.env.y || 0
- );
- // 炸弹图像
- this.bombImage = this.game.add.sprite(0, 0, 'bomb');
- this.bombImage.setOrigin(0.5, 0.5);
- // 烟雾
- this.bombSmoke = this.game.add.sprite(-55, -55, 'smoke');
- // 创建粒子纹理
- const bitmap = this.game.make.graphics({ x: 0, y: 0, add: false });
- this.generateFlame(bitmap);
- const texture = bitmap.generateTexture('flameParticle', 50, 50);
- // 粒子发射器
- this.bombEmit = this.game.add.particles(0, 0, texture.key, {
- x: -30,
- y: -30,
- speed: { min: -100, max: 100 },
- scale: { start: 1, end: 0.8 },
- alpha: { start: 1, end: 0.1 },
- lifespan: 1500,
- frequency: 50,
- maxParticles: 20
- });
- // 添加到容器
- this.sprite.add([this.bombImage, this.bombEmit, this.bombSmoke]);
- // 物理属性
- this.game.physics.add.existing(this.sprite);
- this.sprite.body.setCollideWorldBounds(false);
- }
- generateFlame(bitmap) {
- const len = 5;
- bitmap.fillStyle(0xffffff);
- bitmap.beginPath();
- bitmap.moveTo(25 + len, 25 - len);
- bitmap.lineTo(25 + len, 25 + len);
- bitmap.lineTo(25 - len, 25 + len);
- bitmap.lineTo(25 - len, 25 - len);
- bitmap.closePath();
- bitmap.fill();
- }
- explode(onWhite, onComplete) {
- // 播放炸弹爆炸音效
- const boomSound = this.game.sound.add('boom');
- boomSound.play({
- volume: 0.7 // 炸弹音效可稍大,增强冲击力
- });
- const lights = [];
- const startDeg = Math.floor(Math.random() * 360);
- // 保存外部this上下文
- const self = this;
- // 创建直射光芒效果
- const maxRays = 25; // 光芒数量
- const rayLength = 400; // 光芒长度
- const rayWidth = 200; // 光芒宽度
- for (let i = 0; i < maxRays; i++) {
- const angle = (i / maxRays) * Math.PI * 2; // 计算每个光芒的角度
- const light = self.game.add.graphics({
- x: self.sprite.x,
- y: self.sprite.y
- });
- // 设置初始透明度和颜色
- const alpha = 0.8 - (Math.random() * 0.3); // 随机透明度,增加自然感
- const hue = 45 - (Math.random() * 20); // 随机色调,从黄色到白色
- const color = Phaser.Display.Color.HSVToRGB(hue / 360, 0.8, 1).color;
- light.alpha = alpha;
- light.fillStyle(color, 1);
- // 创建细长三角形
- light.beginPath();
- light.moveTo(0, 0); // 起点在中心
- light.lineTo(
- Math.cos(angle) * rayLength,
- Math.sin(angle) * rayLength
- ); // 终点在外围
- light.lineTo(
- Math.cos(angle + Math.PI + 0.1) * (rayWidth / 2),
- Math.sin(angle + Math.PI + 0.1) * (rayWidth / 2)
- ); // 底部左点
- light.closePath();
- light.fill();
- light.setDepth(2000);
- lights.push(light);
- // 添加脉动动画效果
- self.game.tweens.add({
- targets: light,
- alpha: alpha * 0.5,
- duration: 800 + Math.random() * 400,
- yoyo: true,
- repeat: -1,
- ease: 'Sine.easeInOut'
- });
- }
- // 2. 打乱灯光顺序
- mathTool.shuffle(lights);
- // 3. 创建白屏元素
- const whiteScreen = self.game.add.graphics({
- x: 0,
- y: 0
- });
- whiteScreen.fillStyle(0xffffff, 0);
- whiteScreen.fillRect(0, 0, width, height);
- whiteScreen.setDepth(3000);
- whiteScreen.alpha = 0;
- // 4. 按顺序执行灯光动画
- function playChainAnimations(index) {
- if (index >= lights.length) {
- playWhiteScreenAnimation();
- return;
- }
- const light = lights[index];
- self.game.tweens.add({
- targets: light,
- alpha: { from: 0, to: 1, from: 0 },
- scale: { from: 1, to: 2 },
- duration: 100,
- onComplete: () => {
- light.destroy();
- playChainAnimations(index + 1);
- }
- });
- }
- function playWhiteScreenAnimation() {
- self.game.tweens.add({
- targets: whiteScreen,
- alpha: { from: 0, to: 1, to: 0 },
- duration: 50,
- onUpdate: (tween) => {
- if (tween.progress >= 0.25 && tween.progress <= 0.3) {
- onWhite();
- }
- },
- onComplete: () => {
- whiteScreen.destroy();
- if (typeof onComplete === 'function') {
- onComplete();
- }
- }
- });
- }
- // 开始执行第一个灯光动画
- playChainAnimations(0);
- // 新增:在爆炸动画开始后立即隐藏炸弹,动画结束后销毁
- this.sprite.visible = false; // 先隐藏视觉元素
- // 在爆炸完成回调中销毁炸弹
- const originalOnComplete = onComplete;
- onComplete = () => {
- // 销毁炸弹容器及其内部所有元素
- if (this.sprite) {
- this.sprite.destroy();
- }
- if (this.bombEmit) {
- this.bombEmit.destroy();
- }
- if (this.bombSmoke) {
- this.bombSmoke.destroy();
- }
- // 执行原有的完成回调
- if (typeof originalOnComplete === 'function') {
- originalOnComplete();
- }
- };
- }
- getSprite() {
- return this.sprite;
- }
- }
- // 水果类
- class Fruit {
- constructor(envConfig) {
- this.env = envConfig;
- this.game = envConfig.scene;
- this.sprite = null;
- this.emitterMap = {
- "apple": 0xFFC3E925,
- "banana": 0xFFFFE337,
- "basaha": 0xFFEB2D13,
- "peach": 0xFFF8C928,
- "sandia": 0xFF739E0F
- };
- this.bitmap = null;
- this.emitter = null;
- this.halfOne = null;
- this.halfTwo = null;
- this.init();
- }
- init() {
- this.sprite = this.game.add.sprite(
- this.env.x || 0,
- this.env.y || 0,
- this.env.key
- );
- this.sprite.setOrigin(0.5, 0.5);
- this.sprite.setScale(scaleRatio.value);
- // 物理属性
- this.game.physics.add.existing(this.sprite);
- this.sprite.body.setCollideWorldBounds(false);
- this.sprite.body.onWorldBounds = true;
- // 创建粒子纹理
- this.bitmap = this.game.make.graphics({ x: 0, y: 0, add: false });
- // 粒子发射器
- this.emitter = this.game.add.particles(0, 0, 'flameParticle', {
- visible: false // 初始隐藏
- });
- }
- // 水果类中的half方法
- half(deg, shouldEmit = false) {
- // 计算世界坐标
- const transform = this.sprite.getWorldTransformMatrix();
- const worldPos = new Phaser.Math.Vector2(
- transform.getX(0, 0),
- transform.getY(0, 0)
- );
- // 播放切割音效
- if (shouldEmit) { // 仅在需要发射粒子时播放(即有效切割时)
- const splatterSound = this.game.sound.add('splatter');
- splatterSound.play({
- volume: 0.5 // 可调节音量,范围0-1
- });
- }
- // 1. 计算切割方向的垂直向量(用于分离力)
- // 刀刃角度转弧度
- const rad = Phaser.Math.DegToRad(deg);
- // 垂直于刀刃的方向向量(单位向量)
- const sepX = Math.sin(rad); // 垂直方向X分量
- const sepY = -Math.cos(rad); // 垂直方向Y分量
- // 分离力度(可根据水果大小调整)
- const sepForce = 300;
- // 2. 创建第一半水果
- this.halfOne = this.game.add.sprite(worldPos.x, worldPos.y, this.sprite.texture.key + '-1');
- this.halfOne.setOrigin(0.5, 0.5);
- this.halfOne.setScale(scaleRatio.value);
- this.halfOne.rotation = Phaser.Math.DegToRad(deg + 45); // 初始角度与刀刃匹配
- this.game.physics.add.existing(this.halfOne);
- // 速度 = 原速度 + 分离速度(向一侧)
- this.halfOne.body.velocity.x = this.sprite.body.velocity.x + sepX * sepForce;
- this.halfOne.body.velocity.y = this.sprite.body.velocity.y + sepY * sepForce;
- this.halfOne.body.gravity.y = 2000;
- // 增加旋转(顺时针)
- this.halfOne.body.angularVelocity = 500; // 旋转速度(度/秒)
- this.halfOne.body.setCollideWorldBounds(false);
- this.halfOne.checkWorldBounds = true;
- this.halfOne.outOfBoundsKill = true;
- // 3. 创建第二半水果(分离方向相反)
- this.halfTwo = this.game.add.sprite(worldPos.x, worldPos.y, this.sprite.texture.key + '-2');
- this.halfTwo.setOrigin(0.5, 0.5);
- this.halfTwo.setScale(scaleRatio.value);
- this.halfTwo.rotation = Phaser.Math.DegToRad(deg + 45);
- this.game.physics.add.existing(this.halfTwo);
- // 速度 = 原速度 - 分离速度(向另一侧)
- this.halfTwo.body.velocity.x = this.sprite.body.velocity.x - sepX * sepForce;
- this.halfTwo.body.velocity.y = this.sprite.body.velocity.y - sepY * sepForce;
- this.halfTwo.body.gravity.y = 2000;
- // 增加旋转(逆时针,与第一半相反)
- this.halfTwo.body.angularVelocity = -500;
- this.halfTwo.body.setCollideWorldBounds(false);
- this.halfTwo.checkWorldBounds = true;
- this.halfTwo.outOfBoundsKill = true;
- // 4. 原水果透明度渐变消失(替代直接销毁)
- this.game.tweens.add({
- targets: this.sprite,
- alpha: 0, // 透明度从1→0
- duration: 100, // 100ms内消失
- onComplete: () => {
- this.sprite.destroy();
- }
- });
- // 5. 优化粒子效果(沿分离方向飞溅)
- if (shouldEmit) {
- const emitColor = this.emitterMap[this.sprite.texture.key];
- this.generateFlame(this.bitmap, emitColor);
- const texture = this.bitmap.generateTexture('fruitParticle', 60, 60);
- // 粒子发射器:沿分离方向扩散
- this.emitter = this.game.add.particles(0, 0, texture.key, {
- x: worldPos.x,
- y: worldPos.y,
- // 速度方向:以分离方向为中心,±30度范围
- angle: {
- min: deg - 30,
- max: deg + 30
- },
- speed: { min: 100, max: 300 }, // 速度与分离力度匹配
- scale: { start: 0.8, end: 0.1 },
- alpha: { start: 1, end: 0.1 },
- lifespan: 600, // 延长粒子生命周期,增强视觉效果
- maxParticles: 15 // 增加粒子数量
- });
- }
- }
- generateFlame(bitmap, color) {
- // const len = 30;
- // bitmap.clear();
- // const rgb = Phaser.Display.Color.IntegerToRGB(color);
- // const radgrad = bitmap.context.createRadialGradient(len, len, 4, len, len, len);
- // radgrad.addColorStop(0, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 1)`);
- // radgrad.addColorStop(1, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0)`);
- // bitmap.fillStyle(radgrad);
- // bitmap.fillRect(0, 0, 2 * len, 2 * len);
- const len = 30;
- bitmap.clear();
- // 将 16 进制颜色转换为 RGB 值(0-255 范围)
- const rgb = Phaser.Display.Color.IntegerToRGB(color);
- const r = rgb.r / 255; // 转换为 0-1 范围
- const g = rgb.g / 255;
- const b = rgb.b / 255;
- // 使用 Phaser 的径向渐变 API:fillGradientStyle
- // 参数说明:
- // 1. 渐变类型:1 表示径向渐变
- // 2-5. 内圆中心坐标 (x1, y1) 和半径 (r1)
- // 6-9. 外圆中心坐标 (x2, y2) 和半径 (r2)
- // 10-13. 内圆颜色(r, g, b, a)
- // 14-17. 外圆颜色(r, g, b, a)
- bitmap.fillGradientStyle(
- 1, // 径向渐变
- len, len, 4, // 内圆:中心 (len, len),半径 4
- len, len, len, // 外圆:中心 (len, len),半径 len
- r, g, b, 1, // 内圆颜色(不透明)
- r, g, b, 0 // 外圆颜色(透明)
- );
- // 绘制矩形作为粒子纹理
- bitmap.fillRect(0, 0, 2 * len, 2 * len);
- }
- getSprite() {
- return this.sprite;
- }
- }
- // 刀身类
- class Blade {
- constructor(envConfig, handType) {// 新增handType区分左右手
- this.env = envConfig;
- this.game = envConfig.scene;
- this.points = []; // 记录鼠标轨迹点
- this.graphics = null;
- this.POINTLIFETIME = 200; // 轨迹点的生命周期(毫秒)
- this.allowBlade = false;
- this.lastPoint = null; // 上一个记录的点
- this.moveThreshold = 5; // 鼠标移动超过这个距离才记录新点(避免密集冗余)
- this.handType = handType; // 存储"left"或"right"
- this.color = handType === 'left' ? 0x00FFFF : 0xFFFF00; // 左手蓝色,右手黄色(示例)
- this.init();
- }
- init() {
- this.graphics = this.game.add.graphics({
- x: 0,
- y: 0
- });
- // 根据手型设置绘图层级(可选)
- this.graphics.setDepth(this.handType === 'left' ? 1001 : 1002);
- // 监听鼠标移动事件(可选,也可在update中处理)
- this.game.input.on('pointermove', (pointer) => {
- //console.log("11111", pointer)
- if (this.allowBlade) {
- this.handleMouseMove(pointer);
- }
- });
- }
- // 处理鼠标移动:记录轨迹点
- handleMouseMove(pointer) {
- if (!this.allowBlade) return;
- const point = {
- x: pointer.x,
- y: pointer.y,
- time: Date.now()
- };
- if (!this.lastPoint) {
- // 首次记录点
- this.lastPoint = point;
- this.points.push(point);
- } else {
- // 计算与上一个点的距离,超过阈值才记录新点
- const dis = Math.hypot(
- point.x - this.lastPoint.x,
- point.y - this.lastPoint.y
- );
- if (dis > this.moveThreshold) {
- this.lastPoint = point;
- this.points.push(point);
- }
- }
- }
- update() {
- if (!this.allowBlade) return;
- this.graphics.clear();
- const now = Date.now();
- this.points = this.points.filter(point => now - point.time < this.POINTLIFETIME);
- if (this.points.length > 0) {
- const bladePoints = mathTool.generateBlade(this.points);
- if (bladePoints.length > 0) {
- this.graphics.fillStyle(this.color, 0.8); // 使用当前刀光的颜色
- this.graphics.beginPath();
- this.graphics.moveTo(bladePoints[0].x, bladePoints[0].y);
- bladePoints.forEach((point, i) => i > 0 && this.graphics.lineTo(point.x, point.y));
- this.graphics.closePath();
- this.graphics.fill();
- }
- }
- }
- // 碰撞检测:去掉“鼠标按下”的限制,只要有轨迹就检测
- checkCollide(sprite, onCollide) {
- if (this.allowBlade && this.points.length > 2) { // 仅保留轨迹点数量的判断
- const bounds = sprite.getBounds();
- for (const point of this.points) {
- if (Phaser.Geom.Rectangle.Contains(bounds, point.x, point.y)) {
- onCollide();
- break;
- }
- }
- }
- }
- collideDeg() {
- let deg = 0;
- const len = this.points.length;
- if (len >= 2) {
- const p0 = this.points[0];
- const p1 = this.points[len - 1];
- if (p0.x === p1.x) {
- deg = 90;
- } else {
- const val = (p0.y - p1.y) / (p0.x - p1.x);
- deg = Math.round(Math.atan(val) * 180 / Math.PI);
- }
- if (deg < 0) {
- deg += 180;
- }
- }
- return deg;
- }
- enable() {
- this.allowBlade = true;
- }
- }
- // 启动场景
- class BootScene extends Phaser.Scene {
- constructor() {
- super('boot');
- }
- preload() {
- this.load.image('loading', 'static/images/fruit/preloader.gif');
- }
- create() {
- this.scene.start('preload');
- }
- }
- // 预加载场景
- class PreloadScene extends Phaser.Scene {
- constructor() {
- super('preload');
- }
- preload() {
- this.add.sprite(10, height / 2, 'loading').setPosition((width) / 2, height / 2);
- this.load.on('progress', (value) => {
- // console.log("进度", value)
- });
- // 加载游戏资源
- this.load.image('apple', 'static/images/fruit/apple.png');
- this.load.image('apple-1', 'static/images/fruit/apple-1.png');
- this.load.image('apple-2', 'static/images/fruit/apple-2.png');
- this.load.image('background', 'static/images/fruit/background.jpg');
- this.load.image('banana', 'static/images/fruit/banana.png');
- this.load.image('banana-1', 'static/images/fruit/banana-1.png');
- this.load.image('banana-2', 'static/images/fruit/banana-2.png');
- this.load.image('basaha', 'static/images/fruit/basaha.png');
- this.load.image('basaha-1', 'static/images/fruit/basaha-1.png');
- this.load.image('basaha-2', 'static/images/fruit/basaha-2.png');
- this.load.image('best', 'static/images/fruit/best.png');
- this.load.image('bomb', 'static/images/fruit/bomb.png');
- this.load.image('game-over', 'static/images/fruit/game-over.png');
- this.load.image('home-desc', 'static/images/fruit/home-desc.png');
- this.load.image('home-mask', 'static/images/fruit/home-mask.png');
- this.load.image('logo', 'static/images/fruit/logo.png');
- this.load.image('lose', 'static/images/fruit/lose.png');
- this.load.image('new-game', 'static/images/fruit/new-game.png');
- this.load.image('peach', 'static/images/fruit/peach.png');
- this.load.image('peach-1', 'static/images/fruit/peach-1.png');
- this.load.image('peach-2', 'static/images/fruit/peach-2.png');
- this.load.image('quit', 'static/images/fruit/quit.png');
- this.load.image('sandia', 'static/images/fruit/sandia.png');
- this.load.image('sandia-1', 'static/images/fruit/sandia-1.png');
- this.load.image('sandia-2', 'static/images/fruit/sandia-2.png');
- this.load.image('score', 'static/images/fruit/score.png');
- this.load.image('shadow', 'static/images/fruit/shadow.png');
- this.load.image('smoke', 'static/images/fruit/smoke.png');
- this.load.image('x', 'static/images/fruit/x.png');
- this.load.image('xf', 'static/images/fruit/xf.png');
- this.load.image('xx', 'static/images/fruit/xx.png');
- this.load.image('xxf', 'static/images/fruit/xxf.png');
- this.load.image('xxx', 'static/images/fruit/xxx.png');
- this.load.image('xxxf', 'static/images/fruit/xxxf.png');
- this.load.bitmapFont('number', 'static/images/fruit/bitmapFont.png', 'static/images/fruit/bitmapFont.xml');
- this.load.audio('splatter', 'static/images/fruit/splatter.mp3');
- this.load.audio('boom', 'static/images/fruit/boom.mp3');
- this.load.audio('throw', 'static/images/fruit/throw.mp3');
- }
- create() {
- this.scene.start('main');
- }
- }
- // 主场景
- class MainScene extends Phaser.Scene {
- constructor() {
- super('main');
- this.bg = null;
- this.blade = null;
- this.homeGroup = null;
- this.home_mask = null;
- this.logo = null;
- this.home_desc = null;
- this.sandiaGroup = null;
- this.new_game = null;
- this.sandia = null;
- this.lose = null;
- this.start = false;
- this.sandiaRotateSpeed = 0.9;
- this.newGameRotateSpeed = -0.3;
- this.leftBlade = null; // 左手刀光
- this.rightBlade = null; // 右手刀光
- }
- create() {
- // 初始化物理系统
- this.physics.world.gravity.y = 0;
- // 背景
- this.bg = this.add.image(0, 0, "background");
- this.bg.setScale(wRatio, hRatio);
- this.bg.setPosition(width / 2, height / 2);
- this.bg.setOrigin(0.5, 0.5);
- // 刀光
- this.blade = new Blade({
- scene: this
- });
- // 开始动画
- this.homeGroupAnim();
- // 初始化左右手刀光
- this.leftBlade = new Blade({ scene: this }, 'left');
- this.rightBlade = new Blade({ scene: this }, 'right');
- }
- update() {
- this.updateRotate();
- // 检查是否该跳转到游戏场景
- // if (this.start) {
- // this.gotoNextScene();
- // }
- // 更新刀光
- this.blade.update();
- // 检查水果碰撞
- if (this.sandia && this.sandia.getSprite() && this.sandia.getSprite().active && !this.start) {
- this.blade.checkCollide(
- this.sandia.getSprite(),
- () => {
- this.startGame();
- }
- );
- }
- // 分别更新左右手刀光
- this.leftBlade.update();
- this.rightBlade.update();
- // 分别检测左右手刀光与水果的碰撞
- if (this.sandia && this.sandia.getSprite() && !this.start) {
- // 右手碰撞检测
- this.rightBlade.checkCollide(
- this.sandia.getSprite(),
- () => this.startGame()
- );
- // 左手碰撞检测(可选:允许左手也能启动游戏)
- this.leftBlade.checkCollide(
- this.sandia.getSprite(),
- () => this.startGame()
- );
- }
- }
- homeGroupAnim() {
- //创建组合默认先隐藏
- this.homeGroup = this.add.container(0, -height);
- //背景蒙版
- // this.home_mask = this.add.image(0, 0, "home-mask");
- // this.home_mask.setOrigin(0, 0);
- // this.home_mask.setScale(wRatio);
- // this.home_mask.y = -200;
- //logo
- this.logo = this.add.image(20, 50, "logo");
- this.logo.setOrigin(0, 0);
- //提示语
- this.home_desc = this.add.image(0, 0, "home-desc");
- this.home_desc.setPosition((width - this.home_desc.width / 2) - 20, 70);
- //合并图层
- //this.homeGroup.add([this.home_mask, this.logo, this.home_desc]);
- this.homeGroup.add([this.logo, this.home_desc]);
- // 退出按钮
- this.lose = this.add.image(0, 0, "lose");
- this.lose.setPosition(width - this.lose.width - 30, height - this.lose.height - 30);
- this.lose.setInteractive();
- // 绑定点击事件
- this.lose.on('pointerdown', () => this.getExit());
- //动画效果,接着展示西瓜
- this.tweens.add({
- targets: this.homeGroup,
- y: 0,
- duration: 400,
- ease: 'Sine.InOut',
- onComplete: () => this.fruitAnim()
- });
- }
- fruitAnim() {
- // 每次创建全新的容器,避免复用旧实例
- this.sandiaGroup = this.add.container(0, 0); // 先置0,后续重新计算
- // 西瓜组初始位置:基于当前窗口尺寸动态计算(核心修复)
- this.sandiaGroup.setPosition(width / 2, height / 2); // 强制设置位置
- //圆圈
- this.new_game = this.add.sprite(0, 0, "new-game");
- this.new_game.setOrigin(0.5, 0.5);
- this.new_game.setScale(scaleRatio.value);
- //西瓜
- this.sandia = new Fruit({ scene: this, key: "sandia" });
- this.sandiaGroup.add([this.new_game, this.sandia.getSprite()]);
- //动画效果,接着开放鼠标事件
- this.tweens.add({
- targets: this.sandiaGroup,
- scale: 1,
- duration: 500,
- ease: 'Linear.None',
- onComplete: () => this.allowBlade()
- });
- }
- updateRotate() {
- //西瓜外框圆圈图片旋转
- if (this.new_game) {
- this.new_game.rotation += this.newGameRotateSpeed * 0.016;
- }
- if (this.sandia && this.sandia.getSprite()) {
- this.sandia.getSprite().rotation += this.sandiaRotateSpeed * 0.016;
- }
- }
- allowBlade() {
- this.blade.enable();
- // 同时启用左右手刀光
- this.leftBlade.enable();
- this.rightBlade.enable();
- }
- startGame() {
- // this.start = true;
- // // 隐藏主界面元素
- // this.tweens.add({
- // targets: this.homeGroup,
- // y: -height,
- // duration: 200,
- // ease: 'Sine.InOut'
- // });
- // // 隐藏按钮
- // this.new_game.destroy();
- // this.lose.destroy();
- // // 切开西瓜
- // const deg = this.blade.collideDeg();
- // this.sandia.half(deg);
- this.start = true; // 先禁用立即切换
- const deg = this.blade.collideDeg();
- // 播放初始西瓜切割音效
- const splatterSound = this.sound.add('splatter');
- splatterSound.play({
- volume: 0.5 // 与普通水果切割音量保持一致
- });
- this.sandia.half(deg); // 切开西瓜,生成两半
- // 延迟1秒(1000ms)后再切换场景,等待动画展示
- setTimeout(() => {
- this.gotoNextScene();
- }, 1000);
- }
- gotoNextScene() {
- this.resetScene();
- this.scene.start("play");
- }
- getExit() {
- console.log("退出");
- router.push({ path: '/game' });
- }
- resetScene() {
- this.sandia = null;
- this.start = false;
- // 新增:销毁西瓜容器,避免残留
- if (this.sandiaGroup) {
- this.sandiaGroup.destroy(); // 销毁容器及其子元素
- this.sandiaGroup = null; // 置空引用
- }
- }
- }
- // 游戏场景
- class PlayScene extends Phaser.Scene {
- constructor() {
- super('play');
- this.bg = null;
- this.blade = null;
- this.fruits = [];
- this.score = 0;
- this.playing = false; // 改为false,在初始化方法中设置为true
- this.bombExplode = false;
- this.lostCount = 0;
- this.scoreImage = null;
- this.best = null;
- this.scoreText = null;
- this.xxxGroup = null;
- this.x = null;
- this.xx = null;
- this.xxx = null;
- this.gravity = 200;
- this.leftBlade = null;
- this.rightBlade = null;
- }
- create() {
- // 物理系统
- this.physics.world.gravity.y = this.gravity;
- // 背景
- // this.bg = this.add.image(0, 0, 'background');
- // this.bg.setScale(wRatio, hRatio);
- // this.bg.setPosition(width / 2, height / 2);
- // this.bg.setOrigin(0, 0);
- this.bg = this.add.image(0, 0, "background");
- // 设置背景图铺满整个游戏区域
- this.bg.displayWidth = width;
- this.bg.displayHeight = height;
- // 设置背景图原点为左上角
- this.bg.setOrigin(0, 0);
- // 刀光
- this.blade = new Blade({
- scene: this
- });
- this.blade.enable();
- // 初始化UI
- this.scoreAnim();
- this.scoreTextAnim();
- this.bestAnim();
- this.xxxAnim();
- // 添加调试信息
- console.log("PlayScene created");
- // 调用初始化方法,而不是直接开始生成水果
- this.initGame();
- // 初始化左右手刀光
- this.leftBlade = new Blade({ scene: this }, 'left');
- this.rightBlade = new Blade({ scene: this }, 'right');
- this.leftBlade.enable();
- this.rightBlade.enable();
- }
- initGame() {
- // 重置游戏状态
- this.fruits = [];
- this.score = 0;
- this.lostCount = 0;
- this.bombExplode = false;
- this.scoreText.setText(this.score.toString());
- // 重置失去计数UI
- if (this.xxxGroup) {
- this.xxxGroup.removeAll(true);
- this.x = this.add.image(0, 0, 'x');
- this.xx = this.add.image(22, 0, 'xx');
- this.xxx = this.add.image(49, 0, 'xxx');
- this.x.setOrigin(0, 0);
- this.xx.setOrigin(0, 0);
- this.xxx.setOrigin(0, 0);
- this.xxxGroup.add([this.x, this.xx, this.xxx]);
- }
- // 开始游戏
- this.playing = true;
- console.log("Game initialized and started");
- // 延迟一点时间再生成第一个水果,让玩家有准备
- this.time.delayedCall(1000, () => {
- this.startFruit();
- console.log("First fruit spawned");
- });
- }
- update() {
- // 如果游戏未开始,不执行任何操作
- if (!this.playing) return;
- // 检查是否有水果出界
- if (!this.bombExplode) {
- for (let i = this.fruits.length - 1; i >= 0; i--) {
- const fruit = this.fruits[i];
- const sprite = fruit.getSprite();
- if (sprite && !sprite.active) continue;
- if (sprite && (
- sprite.y > height + 100 ||
- sprite.x < -100 ||
- sprite.x > width + 100
- )) {
- if (fruit.isFruit) {
- this.onOut(fruit);
- }
- sprite.destroy();
- this.fruits.splice(i, 1);
- }
- }
- }
- // 如果没有水果且游戏进行中,生成新水果
- if (this.playing && this.fruits.length === 0 && !this.bombExplode) {
- this.startFruit();
- }
- // 更新刀光
- this.blade.update();
- // 分别更新左右手刀光
- this.leftBlade.update();
- this.rightBlade.update();
- // 检查碰撞
- if (!this.bombExplode) {
- this.fruits.forEach((fruit, i) => {
- if (fruit.getSprite() && fruit.getSprite().active) {
- this.blade.checkCollide(
- fruit.getSprite(),
- () => {
- if (fruit.isFruit) {
- this.onKill(fruit);
- this.fruits.splice(i, 1);
- } else {
- this.onBomb(fruit);
- }
- }
- );
- // 右手碰撞
- this.rightBlade.checkCollide(
- fruit.getSprite(),
- () => this.handleCollision(fruit, i)
- );
- // 左手碰撞
- this.leftBlade.checkCollide(
- fruit.getSprite(),
- () => this.handleCollision(fruit, i)
- );
- }
- });
- }
- }
- // 统一处理碰撞逻辑(提取重复代码)
- handleCollision(fruit, index) {
- if (fruit.isFruit) {
- this.onKill(fruit);
- this.fruits.splice(index, 1);
- } else {
- this.onBomb(fruit);
- }
- }
- scoreAnim() {
- this.scoreImage = this.add.image(-100, 8, 'score');
- this.scoreImage.setOrigin(0, 0);
- this.tweens.add({
- targets: this.scoreImage,
- x: 8,
- duration: 300,
- ease: 'Sine.InOut'
- });
- }
- bestAnim() {
- this.best = this.add.image(-100, 52, 'best');
- this.best.setOrigin(0, 0);
- this.tweens.add({
- targets: this.best,
- x: 5,
- duration: 300,
- ease: 'Sine.InOut'
- });
- }
- scoreTextAnim() {
- this.scoreText = this.add.bitmapText(-100, 40, 'number', this.score.toString(), 32);
- this.scoreText.setOrigin(0, 0);
- this.tweens.add({
- targets: this.scoreText,
- x: 75,
- duration: 300,
- ease: 'Sine.InOut'
- });
- }
- xxxAnim() {
- this.xxxGroup = this.add.container(width + 100, 5);
- this.x = this.add.image(0, 0, 'x');
- this.xx = this.add.image(22, 0, 'xx');
- this.xxx = this.add.image(49, 0, 'xxx');
- this.xxxGroup.add([this.x, this.xx, this.xxx]);
- this.tweens.add({
- targets: this.xxxGroup,
- x: width - 86,
- duration: 300,
- ease: 'Sine.InOut'
- });
- }
- startFruit() {
- // 根据分数动态调整数量:分数越高,生成越多水果
- let min = 1;
- let max = 1;
- if (this.score >= 0 && this.score < 30) {
- min = 1;
- max = 1;
- } else if (this.score >= 30 && this.score < 60) {
- min = 1;
- max = 2;
- } else {
- min = 2;
- max = 3;
- }
- const number = Math.floor(mathTool.randomMinMax(min, max + 1)); // +1是因为randomMinMax的max是 exclusive
- const hasBomb = Math.random() > 0.9;
- const bombIndex = hasBomb ? Math.floor(Math.random() * number) : -1;
- for (let i = 0; i < number; i++) {
- if (i === bombIndex) {
- this.fruits.push(this.randomFruit(false));
- } else {
- this.fruits.push(this.randomFruit(true));
- }
- }
- }
- randomFruit(isFruit) {
- const fruitArray = ["apple", "banana", "basaha", "peach", "sandia"];
- const index = Math.floor(Math.random() * fruitArray.length);
- const x = mathTool.randomPosX();
- const y = mathTool.randomPosY();
- const vx = mathTool.randomVelocityX(x);
- const vy = mathTool.randomVelocityY();
- let fruit;
- if (isFruit) {
- fruit = new Fruit({
- scene: this,
- key: fruitArray[index],
- x: x,
- y: y
- });
- } else {
- fruit = new Bomb({
- scene: this,
- x: x,
- y: y
- });
- }
- console.log("isFruitisFruitisFruit", isFruit)
- fruit.isFruit = isFruit;
- const sprite = fruit.getSprite();
- if (sprite.body) {
- sprite.body.velocity.x = vx;
- sprite.body.velocity.y = vy;
- sprite.body.gravity.y = this.gravity;
- }
- return fruit;
- }
- onOut(fruit) {
- const sprite = fruit.getSprite();
- let x, y;
- // 确定失去标记的位置
- if (sprite.y > height) {
- x = sprite.x;
- y = height - 30;
- } else if (sprite.x < 0) {
- x = 30;
- y = sprite.y;
- } else {
- x = width - 30;
- y = sprite.y;
- }
- // 创建失去标记动画
- const lose = this.add.sprite(x, y, 'lose');
- lose.setOrigin(0.5, 0.5);
- lose.setScale(0);
- const tweenShow = this.tweens.add({
- targets: lose,
- scale: 1,
- duration: 300,
- ease: 'Sine.InOut',
- paused: true
- });
- const tweenHide = this.tweens.add({
- targets: lose,
- scale: 0,
- duration: 300,
- ease: 'Sine.InOut',
- paused: true,
- delay: 1000
- });
- this.tweens.chain({
- targets: lose,
- tweens: [
- {
- scale: 1,
- duration: 300,
- ease: 'Sine.InOut'
- },
- {
- scale: 0,
- duration: 300,
- ease: 'Sine.InOut',
- delay: 1000,
- onComplete: () => {
- lose.destroy();
- }
- }
- ]
- });
- tweenShow.play();
- tweenHide.on('complete', () => {
- lose.destroy();
- });
- this.lostCount++;
- this.loseCount();
- }
- onKill(fruit) {
- const deg = this.blade.collideDeg();
- fruit.half(deg, true);
- this.score++;
- this.scoreText.setText(this.score.toString());
- }
- onBomb(bomb) {
- this.bombExplode = true;
- // 屏幕震动效果
- this.shakeScreen();
- // 停止所有水果的物理运动
- this.fruits.forEach(fruit => {
- if (fruit.getSprite() && fruit.getSprite().body) {
- fruit.getSprite().body.setVelocity(0);
- fruit.getSprite().body.setGravity(0);
- }
- });
- // 炸弹爆炸
- bomb.explode(
- () => {
- // 白屏显示时的回调:销毁所有水果
- this.fruits.forEach(fruit => {
- if (fruit.getSprite()) {
- fruit.getSprite().destroy();
- }
- });
- this.fruits = [];
- },
- () => {
- // 爆炸完成后的回调
- this.gameOver();
- }
- );
- }
- // 添加屏幕震动方法
- shakeScreen() {
- // 获取主相机
- const camera = this.cameras.main;
- // 保存相机初始位置
- const startX = camera.x;
- const startY = camera.y;
- // 震动持续时间(ms)
- const duration = 2000;
- // 震动强度
- const intensity = 8;
- // 震动频率控制
- const frequency = 20;
- // 计算震动次数
- const shakes = duration / frequency;
- let shakeCount = 0;
- // 创建震动定时器
- const shakeInterval = setInterval(() => {
- if (shakeCount < shakes) {
- // 随机生成震动偏移量
- const offsetX = Phaser.Math.Between(-intensity, intensity);
- const offsetY = Phaser.Math.Between(-intensity, intensity);
- // 应用震动
- camera.setPosition(startX + offsetX, startY + offsetY);
- shakeCount++;
- } else {
- // 震动结束,恢复相机位置
- clearInterval(shakeInterval);
- camera.setPosition(startX, startY);
- }
- }, frequency);
- }
- loseCount() {
- if (this.lostCount === 1) {
- this.lostAnim(this.x, 'xf');
- } else if (this.lostCount === 2) {
- this.lostAnim(this.xx, 'xxf');
- } else if (this.lostCount >= 3) {
- this.lostAnim(this.xxx, 'xxxf');
- this.gameOver();
- }
- }
- lostAnim(removeObj, addKey) {
- removeObj.destroy();
- const newObj = this.add.sprite(removeObj.x, removeObj.y, addKey);
- newObj.setOrigin(0, 0);
- newObj.setScale(0);
- this.xxxGroup.add(newObj);
- this.tweens.add({
- targets: newObj,
- scale: 1,
- duration: 300,
- ease: 'Sine.InOut'
- });
- }
- gameOver() {
- this.playing = false;
- // 1. 确保所有其他元素停止更新,避免干扰
- this.blade.allowBlade = false; // 禁用刀光
- // 2. 创建game-over图片,并设置最高层级
- const gameOverSprite = this.add.sprite(width / 2, height / 2, 'game-over');
- gameOverSprite.setOrigin(0.5, 0.5);
- gameOverSprite.setScale(0);
- gameOverSprite.setDepth(1000); // 设置最高层级,确保不被覆盖
- // 3. 优化入场动画,确保平滑显示
- this.tweens.add({
- targets: gameOverSprite,
- scale: 1,
- duration: 500, // 延长动画时间,确保可见
- ease: 'Elastic.Out', // 更明显的弹性动画,增强视觉效果
- onComplete: () => {
- // 动画完成后再设置自动返回,确保用户有足够时间看到画面
- setTimeout(() => {
- console.log('游戏结束,返回首页');
- this.scene.start('main');
- }, 1000); // 延长至3秒,给用户足够时间观察
- }
- });
- // 4. 支持点击立即返回,提升交互体验
- gameOverSprite.setInteractive();
- gameOverSprite.on('pointerdown', () => {
- console.log('点击返回首页');
- this.scene.start('main');
- });
- }
- }
- // 外部方法(如Vue组件中的某个按钮点击事件)
- const externalMethod = (autoX, autoY) => {
- // 1. 获取游戏实例(从之前保存的gameRef中)
- const game = gameRef.value;
- if (!game) {
- console.error("游戏未初始化");
- return;
- }
- // 2. 获取目标场景(根据当前活跃场景选择'main'或'play')
- // 例如:获取PlayScene(场景key为'play')
- const currentScene = getActiveScene();
- const targetScene = game.scene.getScene(currentScene);
- // 若当前在主场景,可改为 game.scene.getScene('main')
- // 3. 检查场景中的Blade实例是否存在且已启用
- if (!targetScene || !targetScene.blade || !targetScene.blade.allowBlade) {
- console.error("Blade实例未初始化或未启用");
- return;
- }
- // 4. 构造模拟的pointer参数(包含x和y)
- const mockPointer = {
- x: autoX, // 外部传入的X坐标
- y: autoY // 外部传入的Y坐标
- };
- // 5. 调用Blade的handleMouseMove方法
- targetScene.blade.handleMouseMove(mockPointer);
- };
- // 新增:区分左右手的输入方法
- const externalLeftHandMethod = (x, y) => {
- const game = gameRef.value;
- if (!game) return;
- const currentScene = getActiveScene();
- const targetScene = game.scene.getScene(currentScene);
- if (targetScene && targetScene.leftBlade && targetScene.leftBlade.allowBlade) {
- targetScene.leftBlade.handleMouseMove({ x, y });
- }
- };
- const externalRightHandMethod = (x, y) => {
- const game = gameRef.value;
- if (!game) return;
- const currentScene = getActiveScene();
- const targetScene = game.scene.getScene(currentScene);
- if (targetScene && targetScene.rightBlade && targetScene.rightBlade.allowBlade) {
- targetScene.rightBlade.handleMouseMove({ x, y });
- }
- };
- const getActiveScene = () => {
- const game = gameRef.value; // 获取游戏实例
- if (!game) return null;
- // 获取所有活跃场景(通常只有一个)
- const activeScenes = game.scene.getScenes(true);
- // 返回第一个活跃场景(如果有)
- return activeScenes.length > 0 ? activeScenes[0].scene.key : null;
- };
- onBeforeMount(() => {
- clientObj.value = {
- width: document.documentElement.clientWidth,
- height: document.documentElement.clientHeight,
- }
- scaleRatio.value = clientObj.value.height / 480
- getInit();
- });
- // 初始化游戏
- onMounted(() => {
- // 获取容器尺寸
- const container = document.getElementById('game');
- // 初始化工具类
- mathTool.init();
- // 创建游戏实例
- game = new Phaser.Game({
- type: Phaser.CANVAS,
- width: width,
- height: height,
- parent: 'game',
- scene: [BootScene, PreloadScene, MainScene, PlayScene],
- physics: {
- default: 'arcade',
- arcade: {
- debug: false
- }
- }
- });
- gameRef.value = game;
- // 模拟自动接收坐标数据(例如WebSocket回调)
- function onReceiveCoordinate(data) {
- // data格式:{ x: 100, y: 200 }
- if (data.x !== undefined && data.y !== undefined) {
- externalMethod(data.x, data.y); // 调用外部方法,传入坐标
- }
- }
- // // 示例:每50ms发送一组随机坐标
- // setInterval(() => {
- // const randomX = Math.random() * width; // width是屏幕宽度
- // const randomY = Math.random() * height; // height是屏幕高度
- // onReceiveCoordinate({ x: randomX, y: randomY });
- // }, 50);
- });
- onBeforeUnmount(() => {
- closeWS();
- });
- </script>
- <style lang="scss" scoped></style>
|