football.vue 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832
  1. <template>
  2. <div class="game-container">
  3. <div id="gameCanvas" class="game-canvas"></div>
  4. <!-- <canvas ref="canvasRef" :width="clientObj.width" :height="clientObj.height"
  5. style="position:fixed;left: 0; top: 0; z-index: 999;"></canvas> -->
  6. <!-- 游戏启动界面 -->
  7. <div v-if="currentScene === 'start'" class="gamestart">
  8. <img v-if="currentScene === 'start'" src="/static/images/football/game_start.jpg" class="start_bg" />
  9. <div class="btn rule" @click="showRules = true">查看规则</div>
  10. <div class="btn start" @click="startGame">踢腿开始游戏</div>
  11. <!-- 规则弹窗 -->
  12. <div v-if="showRules" class="ruleshadow">
  13. <div class="rulebox">
  14. <span class="x_rulebox" @click="showRules = false"></span>
  15. <h3>操作方法:</h3>
  16. <p>手指拖动控制人物带球奔跑的方向和速度。</p>
  17. <h3>道具说明:</h3>
  18. <div class="daoju">
  19. <div class="daoju_item">
  20. <span class="daoju_1"></span>
  21. <p>每收集一个,生命值+1;触碰障碍物,生命可再复活。</p>
  22. </div>
  23. <!-- <div class="daoju_item">
  24. <span class="daoju_2"></span>
  25. <p>每收集一个,能力值+1;扫除面前一切障碍,加速前进。</p>
  26. </div> -->
  27. <div class="daoju_item">
  28. <span class="daoju_3"></span>
  29. <p>触碰锥桶,生命值-1;成功绕过一次分数+1。</p>
  30. </div>
  31. </div>
  32. </div>
  33. </div>
  34. </div>
  35. <!-- 规则说明 -->
  36. <div v-if="currentScene === 'rule'" class="zhiyin">
  37. <div class="btn start" @click="continueGame">继续游戏</div>
  38. </div>
  39. <!-- 倒计时 -->
  40. <div v-if="currentScene === 'countdown'" class="daoju">
  41. <div class="daojishibox">
  42. <div class="daojishinum">{{ countdownNum }}</div>
  43. </div>
  44. </div>
  45. <!-- 游戏结束 -->
  46. <div v-if="currentScene === 'gameover'" class="gameend">
  47. <div v-if="currentScene === 'gameover'" class="game_fenshu">
  48. <span>{{ score }}</span>分
  49. </div>
  50. <div class="chenghao">恭喜你获得称号<p>{{ getTitle(score) }}</p>
  51. </div>
  52. <!-- <div class="game_ogain" @click="restartGame">返回游戏</div> -->
  53. </div>
  54. </div>
  55. </template>
  56. <script setup name="Football" lang="ts">
  57. import Phaser from 'phaser';
  58. import { onMounted, ref, reactive, onBeforeUnmount, watch } from 'vue';
  59. import { initSpeech, speckText, playMusic, controlMusic, speckCancel, chineseNumber } from '@/utils/speech';
  60. import { useWebSocket } from '@/utils/bodyposeWs';
  61. const { proxy } = getCurrentInstance() as any;
  62. const router = useRouter();
  63. const { bodyposeWs, startDevice, checkBodypose, openBodypose, terminateBodypose, suspendBodypose, resumeBodypose, getBodyposeState, closeWS } = useWebSocket();
  64. const canvasRef = ref(null);
  65. const data = reactive<any>({
  66. bodyposeData: {},//姿态信息
  67. bodyposeState: false,//姿态识别窗口状态
  68. parameter: {},//参数
  69. deviceInfo: {},//设备信息
  70. againNum: 0,//再次启动次数
  71. againTimer: null,//定时状态
  72. wsState: false,//WS状态
  73. clientObj: {},//浏览器对象
  74. boxes: [],//四个点坐标
  75. proportion: null,//人框和屏幕比例
  76. myThrow: 0,//0踢腿 1收腿
  77. myTimer: null,
  78. direction: null,//跑动
  79. });
  80. const { bodyposeData, bodyposeState, parameter, deviceInfo, againNum, againTimer, wsState, clientObj, boxes, proportion, myThrow, myTimer, direction } = toRefs(data);
  81. // 游戏状态管理
  82. const currentScene = ref('start');
  83. const showRules = ref(false);
  84. const countdownNum = ref(3);
  85. const score = ref(0);
  86. const game = ref(null);
  87. const gameConfig = ref(null);
  88. let scoreListener = null; // 存储事件监听器引用
  89. // 游戏常量
  90. const width = document.documentElement.clientWidth;
  91. const height = document.documentElement.clientHeight;
  92. const GAME_WIDTH = width;
  93. const GAME_HEIGHT = height;
  94. // 游戏资源
  95. const gameAssets = {
  96. images: [
  97. { key: 'gameStart', url: 'static/images/football/game_start.jpg' },
  98. { key: 'grass', url: 'static/images/football/Caopi.png' },
  99. { key: 'playerShoot', url: 'static/images/football/QiuyuanShooting.png' },
  100. { key: 'pile', url: 'static/images/football/Pile.png' },
  101. { key: 'jersey', url: 'static/images/football/Jersey.png' },
  102. // { key: 'broom', url: 'static/images/football/Broom.png' },
  103. { key: 'goalkeeper', url: 'static/images/football/Goalkeeper.png' },
  104. { key: 'goal', url: 'static/images/football/Goal.png' },
  105. { key: 'ball', url: 'static/images/football/Ball.png' },
  106. { key: 'line', url: 'static/images/football/Line.png' },
  107. { key: 'line2', url: 'static/images/football/Line2.png' },
  108. { key: 'goalBackground', url: 'static/images/football/GoalBackGround.png' },
  109. { key: 'gameOver', url: 'static/images/football/gameover.png' }
  110. ],
  111. spritesheets: [
  112. {
  113. key: 'playerAnim',
  114. url: 'static/images/football/Qiuyuan.png',
  115. frameWidth: 92.5,
  116. frameHeight: 92.5
  117. },
  118. {
  119. key: 'ballAnim',
  120. url: 'static/images/football/Ball.png',
  121. frameWidth: 31,
  122. frameHeight: 31
  123. },
  124. {
  125. key: 'goalkeeperAnim',
  126. url: 'static/images/football/Goalkeeper.png',
  127. frameWidth: 55,
  128. frameHeight: 56
  129. },
  130. {
  131. key: 'playerCollision',
  132. url: 'static/images/football/QiuyuanCollision.png',
  133. frameWidth: 92.5,
  134. frameHeight: 92.5
  135. },
  136. {
  137. key: 'playerSuper',
  138. url: 'static/images/football/QiuyuanSuper.png',
  139. frameWidth: 92.5,
  140. frameHeight: 92.5
  141. },
  142. ]
  143. };
  144. // 初始化Phaser游戏
  145. const initGame = () => {
  146. // 游戏配置
  147. gameConfig.value = {
  148. type: Phaser.AUTO,
  149. width: GAME_WIDTH,
  150. height: GAME_HEIGHT,
  151. parent: 'gameCanvas',
  152. physics: {
  153. default: 'arcade',
  154. arcade: {
  155. gravity: { y: 0 },
  156. debug: false
  157. }
  158. },
  159. scene: [
  160. PreloaderScene,
  161. GameScene
  162. ]
  163. };
  164. game.value = new Phaser.Game(gameConfig.value);
  165. // 关键:使用局部变量存储事件监听器,便于后续清理
  166. scoreListener = (newScore) => {
  167. score.value = newScore;
  168. };
  169. // 添加事件监听
  170. if (game.value && game.value.events) {
  171. game.value.events.on('scoreChanged', scoreListener);
  172. }
  173. game.value.events.on('gameOver', () => {
  174. currentScene.value = 'gameover';
  175. setTimeout(() => {
  176. if (currentScene.value == 'gameover') {
  177. restartGame();
  178. }
  179. }, 2000)
  180. });
  181. };
  182. // 预加载场景
  183. class PreloaderScene extends Phaser.Scene {
  184. constructor() {
  185. super('PreloaderScene');
  186. }
  187. preload() {
  188. // 加载图片资源
  189. gameAssets.images.forEach(asset => {
  190. this.load.image(asset.key, asset.url);
  191. });
  192. // 加载精灵表
  193. gameAssets.spritesheets.forEach(asset => {
  194. this.load.spritesheet(asset.key, asset.url, {
  195. frameWidth: asset.frameWidth,
  196. frameHeight: asset.frameHeight
  197. });
  198. });
  199. // 显示加载进度
  200. const progressBar = this.add.graphics();
  201. const progressBox = this.add.graphics();
  202. progressBox.fillStyle(0x222222, 0.8);
  203. progressBox.fillRect(GAME_WIDTH / 2 - 160, GAME_HEIGHT / 2 - 25, 320, 50);
  204. const loadingText = this.make.text({
  205. x: GAME_WIDTH / 2,
  206. y: GAME_HEIGHT / 2 - 50,
  207. text: 'Loading...',
  208. style: {
  209. font: '20px monospace',
  210. fill: '#ffffff'
  211. }
  212. }).setOrigin(0.5, 0.5);
  213. const percentText = this.make.text({
  214. x: GAME_WIDTH / 2,
  215. y: GAME_HEIGHT / 2,
  216. text: '0%',
  217. style: {
  218. font: '18px monospace',
  219. fill: '#ffffff'
  220. }
  221. }).setOrigin(0.5, 0.5);
  222. this.load.on('progress', (value) => {
  223. percentText.setText(`${Math.round(value * 100)}%`);
  224. progressBar.clear();
  225. progressBar.fillStyle(0xffffff, 1);
  226. progressBar.fillRect(GAME_WIDTH / 2 - 150, GAME_HEIGHT / 2 - 15, 300 * value, 30);
  227. });
  228. this.load.on('complete', () => {
  229. progressBar.destroy();
  230. progressBox.destroy();
  231. loadingText.destroy();
  232. percentText.destroy();
  233. });
  234. }
  235. create() {
  236. // 初始化动画
  237. this.initAnimations();
  238. // 切换到游戏场景
  239. this.scene.start('GameScene');
  240. }
  241. initAnimations() {
  242. // 球员动画
  243. this.anims.create({
  244. key: 'playerLeft',
  245. frames: [
  246. { key: 'playerAnim', frame: 0 },
  247. { key: 'playerAnim', frame: 2 },
  248. { key: 'playerAnim', frame: 4 },
  249. { key: 'playerAnim', frame: 6 }
  250. ],
  251. frameRate: 10,
  252. repeat: -1
  253. });
  254. this.anims.create({
  255. key: 'playerRight',
  256. frames: [
  257. { key: 'playerAnim', frame: 1 },
  258. { key: 'playerAnim', frame: 3 },
  259. { key: 'playerAnim', frame: 5 },
  260. { key: 'playerAnim', frame: 7 }
  261. ],
  262. frameRate: 10,
  263. repeat: -1
  264. });
  265. // 足球动画
  266. this.anims.create({
  267. key: 'ballAnim',
  268. frames: this.anims.generateFrameNumbers('ballAnim', { start: 0, end: 1 }),
  269. frameRate: 5,
  270. repeat: -1
  271. });
  272. // 守门员动画
  273. this.anims.create({
  274. key: 'goalkeeperAnim',
  275. frames: this.anims.generateFrameNumbers('goalkeeperAnim', { start: 0, end: 1 }),
  276. frameRate: 3,
  277. repeat: -1
  278. });
  279. }
  280. }
  281. // 游戏主场景
  282. class GameScene extends Phaser.Scene {
  283. constructor() {
  284. super('GameScene');
  285. this.player = null;
  286. this.score = 0;
  287. this.lives = 3;
  288. this.maxLives = 5;
  289. this.speed = 6;
  290. this.acceleration = 1.9;
  291. this.level = 1;
  292. this.obstacles = [];
  293. this.powerUps = [];
  294. this.isSuper = false;
  295. this.gameActive = false;
  296. this.timer = null;
  297. this.isShooting = false; // 新增:初始化射门状态
  298. this.obstacleTimer = null; // 新增:初始化障碍物计时器
  299. this.obstacleEvent = null;
  300. this.jerseyEvent = null;
  301. // this.broomEvent = null;
  302. this.shootTimeout = null; // 射门超时计时器
  303. this.shootTimeLeft = 15; // 剩余射门时间
  304. this.shootTimerText = null; // 倒计时显示文本
  305. }
  306. create() {
  307. // 创建背景
  308. this.createBackground();
  309. // 创建玩家
  310. this.createPlayer();
  311. // 创建计分板
  312. this.createHUD();
  313. // 输入控制
  314. this.initControls();
  315. // 游戏事件
  316. this.events.on('resume', () => {
  317. this.gameActive = true;
  318. });
  319. // 等待开始游戏信号
  320. this.gameActive = false;
  321. }
  322. createBackground() {
  323. // 创建滚动背景
  324. this.background = this.add.tileSprite(0, 0, GAME_WIDTH, GAME_HEIGHT, 'grass');
  325. this.background.setOrigin(0, 0);
  326. }
  327. createPlayer() {
  328. // 创建玩家
  329. //this.player = this.physics.add.sprite(GAME_WIDTH / 2, GAME_HEIGHT - 100, 'playerAnim');
  330. this.player = this.physics.add.sprite(direction.value + 46, GAME_HEIGHT - 100, 'playerAnim');
  331. this.player.setCollideWorldBounds(true);
  332. this.player.setScale(0.8);
  333. this.player.lives = this.lives;
  334. this.player.setDepth(9);
  335. }
  336. createHUD() {
  337. // 分数显示
  338. this.scoreText = this.add.text(10, 10, `分数: ${this.score}`, {
  339. fontSize: '16px',
  340. fill: '#ffffff',
  341. backgroundColor: 'rgba(0,0,0,0.5)',
  342. padding: { x: 5, y: 2 }
  343. });
  344. this.scoreText.setDepth(10);
  345. // 生命值显示
  346. this.livesText = this.add.text(GAME_WIDTH - 80, 10, `生命: ${this.lives}`, {
  347. fontSize: '16px',
  348. fill: '#ffffff',
  349. backgroundColor: 'rgba(0,0,0,0.5)',
  350. padding: { x: 5, y: 2 }
  351. });
  352. this.livesText.setDepth(10);
  353. }
  354. initControls() {
  355. // 键盘控制
  356. this.cursors = this.input.keyboard.createCursorKeys();
  357. // 触摸控制(添加垂直移动)
  358. this.input.on('pointermove', (pointer) => {
  359. if (this.gameActive && pointer.isDown && !this.isShooting) {
  360. // 水平移动
  361. this.player.x = Phaser.Math.Clamp(pointer.x, this.player.width / 2, GAME_WIDTH - this.player.width / 2);
  362. // 垂直移动(限制范围)
  363. this.player.y = Phaser.Math.Clamp(
  364. pointer.y,
  365. 0, // 最小Y值
  366. GAME_HEIGHT// 最大Y值
  367. );
  368. }
  369. });
  370. }
  371. startGame() {
  372. // 重置核心状态
  373. this.gameActive = true;
  374. this.score = 0;
  375. this.lives = 3; // 重置生命值
  376. this.maxLives = 5;
  377. this.speed = 6; // 重置基础速度
  378. this.acceleration = 1.9; // 重置加速度
  379. this.level = 1; // 重置等级
  380. this.isSuper = false;
  381. this.isShooting = false;
  382. this.obstacles = [];
  383. this.powerUps = [];
  384. // 更新UI显示
  385. this.livesText.setText(`生命: ${this.lives}`);
  386. this.updateScore();
  387. // 清理旧定时器(双重保险)
  388. if (this.timer) this.timer.remove();
  389. if (this.obstacleEvent) this.obstacleEvent.remove();
  390. if (this.jerseyEvent) this.jerseyEvent.remove();
  391. // if (this.broomEvent) this.broomEvent.remove();
  392. // 重新开始生成障碍物和计时
  393. this.startSpawning();
  394. this.timer = this.time.addEvent({
  395. delay: 200,
  396. callback: () => {
  397. this.score++;
  398. this.updateScore();
  399. if (this.score % 200 === 0) {
  400. this.levelUp();
  401. }
  402. },
  403. loop: true
  404. });
  405. }
  406. startSpawning() {
  407. // 先移除旧事件(强化清除逻辑)
  408. if (this.obstacleEvent) {
  409. this.obstacleEvent.remove();
  410. this.obstacleEvent = null; // 置空引用
  411. }
  412. if (this.jerseyEvent) {
  413. this.jerseyEvent.remove();
  414. this.jerseyEvent = null; // 置空引用
  415. }
  416. // if (this.broomEvent) {
  417. // this.broomEvent.remove();
  418. // this.broomEvent = null; // 置空引用
  419. // }
  420. // 重新创建事件(使用安全的场景上下文)
  421. this.obstacleEvent = this.time.addEvent({
  422. delay: 1000 / this.level,
  423. callback: () => { if (this.gameActive) this.spawnObstacle(); },
  424. loop: true
  425. });
  426. this.jerseyEvent = this.time.addEvent({
  427. delay: 5000,
  428. callback: () => { if (this.gameActive) this.spawnPowerUp('jersey'); },
  429. loop: true
  430. });
  431. // this.broomEvent = this.time.addEvent({
  432. // delay: 8000,
  433. // callback: () => { if (this.gameActive) this.spawnPowerUp('broom'); },
  434. // loop: true
  435. // });
  436. // 新增:立即生成第一个障碍物,确保倒计时结束后立即出现
  437. this.spawnObstacle();
  438. }
  439. spawnObstacle() {
  440. const x = Phaser.Math.Between(30, GAME_WIDTH - 30);
  441. const obstacle = this.physics.add.sprite(x, -50, 'pile');
  442. obstacle.setScale(0.7);
  443. // 降低速度(从原来的this.speed * this.acceleration * 10调整为)
  444. obstacle.setVelocityY(this.speed * this.acceleration * 5); // 速度减半
  445. obstacle.setDepth(8);
  446. obstacle.type = 'pile';
  447. // 碰撞检测
  448. this.physics.add.overlap(this.player, obstacle, this.handleObstacleCollision, null, this);
  449. this.obstacles.push(obstacle);
  450. // 替换原计时器代码
  451. const event = this.time.addEvent({ // 用变量接收事件引用
  452. delay: 100,
  453. loop: true,
  454. callback: () => {
  455. if (obstacle.active) {
  456. if (obstacle.y > GAME_HEIGHT + obstacle.height) {
  457. obstacle.destroy();
  458. this.obstacles = this.obstacles.filter(o => o !== obstacle);
  459. this.score++;
  460. this.updateScore();
  461. this.time.removeEvent(event); // 移除当前事件
  462. }
  463. } else {
  464. this.time.removeEvent(event);
  465. }
  466. }
  467. });
  468. }
  469. spawnPowerUp(type) {
  470. const x = Phaser.Math.Between(30, GAME_WIDTH - 30);
  471. let powerUp;
  472. if (type === 'jersey') {
  473. powerUp = this.physics.add.sprite(x, -50, 'jersey');
  474. powerUp.type = 'jersey';
  475. }
  476. // else if (type === 'broom') {
  477. // powerUp = this.physics.add.sprite(x, -50, 'broom');
  478. // powerUp.type = 'broom';
  479. // }
  480. powerUp.setScale(0.7);
  481. // 降低道具速度(从原来的this.speed * this.acceleration * 8调整为)
  482. powerUp.setVelocityY(this.speed * this.acceleration * 4); // 速度降低
  483. powerUp.setDepth(8);
  484. // 碰撞检测
  485. this.physics.add.overlap(this.player, powerUp, this.handlePowerUpCollision, null, this);
  486. this.powerUps.push(powerUp);
  487. // 延长道具生命周期
  488. this.time.addEvent({
  489. delay: 15000,
  490. callback: () => {
  491. if (powerUp.active) {
  492. powerUp.destroy();
  493. this.powerUps = this.powerUps.filter(p => p !== powerUp);
  494. }
  495. }
  496. });
  497. }
  498. handleObstacleCollision(player, obstacle) {
  499. if (this.isSuper) {
  500. // 超级状态下直接摧毁障碍物
  501. obstacle.destroy();
  502. this.obstacles = this.obstacles.filter(o => o !== obstacle);
  503. return;
  504. }
  505. // 扣除生命值
  506. player.lives--;
  507. this.lives = player.lives;
  508. this.livesText.setText(`生命: ${this.lives}`);
  509. // 显示受伤效果
  510. player.setTexture('playerCollision');
  511. this.time.addEvent({
  512. delay: 618,
  513. callback: () => {
  514. player.setTexture('playerAnim');
  515. }
  516. });
  517. // 销毁障碍物
  518. obstacle.destroy();
  519. this.obstacles = this.obstacles.filter(o => o !== obstacle);
  520. // 检查游戏是否结束
  521. if (player.lives <= 0) {
  522. this.gameOver();
  523. }
  524. }
  525. handlePowerUpCollision(player, powerUp) {
  526. if (powerUp.type === 'jersey') {
  527. // 增加生命值
  528. if (player.lives < this.maxLives) {
  529. player.lives++;
  530. this.lives = player.lives;
  531. this.livesText.setText(`生命: ${this.lives}`);
  532. }
  533. }
  534. // else if (powerUp.type === 'broom') {
  535. // // 激活超级状态
  536. // this.activateSuperMode();
  537. // }
  538. // 销毁道具
  539. powerUp.destroy();
  540. this.powerUps = this.powerUps.filter(p => p !== powerUp);
  541. }
  542. activateSuperMode() {
  543. this.isSuper = true;
  544. this.player.setTexture('playerSuper');
  545. this.acceleration = 10;
  546. // 3秒后结束超级状态
  547. this.time.addEvent({
  548. delay: 3000,
  549. callback: () => {
  550. this.isSuper = false;
  551. this.player.setTexture('playerAnim');
  552. this.acceleration = 1.9 + (this.level - 1) * 0.5;
  553. }
  554. });
  555. }
  556. levelUp() {
  557. this.level++;
  558. this.acceleration += 0.5;
  559. // 显示升级提示
  560. const levelUpText = this.add.text(GAME_WIDTH / 2, GAME_HEIGHT / 2, `Level ${this.level}!`, {
  561. fontSize: '32px',
  562. fill: '#ffff00',
  563. stroke: '#000000',
  564. strokeThickness: 2
  565. }).setOrigin(0.5);
  566. levelUpText.setDepth(10);
  567. this.time.addEvent({
  568. delay: 1000,
  569. callback: () => {
  570. levelUpText.destroy();
  571. }
  572. });
  573. }
  574. updateScore() {
  575. // 新增:检查游戏是否处于活跃状态
  576. if (!this.gameActive) return;
  577. try {
  578. if (!this.scoreText || !this.scene) return;
  579. this.scoreText.setText(`分数: ${this.score}`);
  580. if (this.game && this.game.events) {
  581. this.game.events.emit('scoreChanged', this.score);
  582. }
  583. if (this.score % 200 === 0 && this.score > 0 && !this.isShooting) {
  584. this.enterShootingMode();
  585. }
  586. } catch (error) {
  587. console.error('updateScore 错误:', error);
  588. }
  589. }
  590. enterShootingMode() {
  591. this.gameActive = false;
  592. this.isShooting = true;
  593. this.shootTimeLeft = 15; // 重置剩余时间
  594. // 暂停计分定时器
  595. if (this.timer) {
  596. this.timer.paused = true;
  597. }
  598. // 重置键盘状态
  599. this.input.keyboard.resetKeys();
  600. this.cursors.left.isDown = false;
  601. this.cursors.right.isDown = false;
  602. this.cursors.up.isDown = false;
  603. this.cursors.down.isDown = false;
  604. // 停止所有生成事件
  605. if (this.obstacleEvent) this.obstacleEvent.remove();
  606. if (this.jerseyEvent) this.jerseyEvent.remove();
  607. // if (this.broomEvent) this.broomEvent.remove();
  608. // 清除现有障碍物和道具
  609. this.obstacles.forEach(obs => obs.destroy());
  610. this.obstacles = [];
  611. this.powerUps.forEach(p => p.destroy());
  612. this.powerUps = [];
  613. // 创建射门场景背景
  614. this.background.setTexture('goalBackground');
  615. // 创建球门
  616. this.goal = this.physics.add.sprite(GAME_WIDTH / 2, 100, 'goal');
  617. this.goal.setScale(0.8);
  618. this.goal.setImmovable(true);
  619. this.goal.setDepth(5);
  620. // 创建守门员
  621. this.goalkeeper = this.physics.add.sprite(GAME_WIDTH / 2, 150, 'goalkeeperAnim');
  622. this.goalkeeper.setScale(0.9);
  623. this.goalkeeper.setImmovable(true);
  624. this.goalkeeper.anims.play('goalkeeperAnim', true);
  625. this.goalkeeper.setDepth(7);
  626. // 让守门员左右移动
  627. this.tweens.add({
  628. targets: this.goalkeeper,
  629. x: [GAME_WIDTH / 2 - 50, GAME_WIDTH / 2 + 50],
  630. duration: 4000,
  631. ease: 'Sine.inOut',
  632. repeat: -1,
  633. yoyo: true
  634. });
  635. // 调整球员位置(准备射门)
  636. this.player.setPosition(GAME_WIDTH / 2, GAME_HEIGHT - 100);
  637. this.player.setTexture('playerShoot');
  638. this.player.setVelocity(0);
  639. // 创建足球
  640. this.ball = this.physics.add.sprite(this.player.x, this.player.y - 50, 'ballAnim');
  641. this.ball.setScale(0.8);
  642. this.ball.anims.play('ballAnim', true);
  643. this.ball.setDepth(8);
  644. // 显示射门提示和倒计时
  645. // this.shootHint = this.add.text(GAME_WIDTH / 2, GAME_HEIGHT - 80, '点击或按空格键射门', {
  646. // fontSize: '16px',
  647. // fill: '#ffffff',
  648. // backgroundColor: 'rgba(0,0,0,0.5)',
  649. // padding: { x: 5, y: 2 }
  650. // }).setOrigin(0.5);
  651. // this.shootHint.setDepth(10);
  652. // 添加射门倒计时显示
  653. this.shootTimerText = this.add.text(GAME_WIDTH / 2, GAME_HEIGHT - 50, `剩余时间: ${this.shootTimeLeft}秒`, {
  654. fontSize: '16px',
  655. fill: '#ffffff',
  656. backgroundColor: 'rgba(0,0,0,0.5)',
  657. padding: { x: 5, y: 2 }
  658. }).setOrigin(0.5);
  659. this.shootTimerText.setDepth(10);
  660. // 设置射门超时计时器
  661. this.shootTimeout = this.time.addEvent({
  662. delay: 1000, // 每秒触发一次
  663. callback: () => {
  664. this.shootTimeLeft--;
  665. this.shootTimerText.setText(`剩余时间: ${this.shootTimeLeft}秒`);
  666. // 时间到未射门,判定失败
  667. if (this.shootTimeLeft <= 0) {
  668. this.handleShootTimeout();
  669. }
  670. },
  671. loop: true
  672. });
  673. // 射门控制
  674. this.input.keyboard.on('keydown-SPACE', this.shootBall, this);
  675. this.input.on('pointerdown', this.shootBall, this);
  676. }
  677. handleShootTimeout() {
  678. // 清除超时计时器
  679. if (this.shootTimeout) {
  680. this.shootTimeout.remove();
  681. this.shootTimeout = null;
  682. }
  683. // 禁用输入
  684. this.input.keyboard.off('keydown-SPACE', this.shootBall, this);
  685. this.input.off('pointerdown', this.shootBall, this);
  686. // 移除提示文本
  687. if (this.shootHint) {
  688. this.shootHint.destroy();
  689. this.shootHint = null;
  690. }
  691. if (this.shootTimerText) {
  692. this.shootTimerText.destroy();
  693. this.shootTimerText = null;
  694. }
  695. // 显示超时提示
  696. const timeoutText = this.add.text(GAME_WIDTH / 2, GAME_HEIGHT / 2, '射门超时!', {
  697. fontSize: '24px',
  698. fill: '#f00',
  699. stroke: '#000000',
  700. strokeThickness: 2
  701. }).setOrigin(0.5);
  702. timeoutText.setDepth(10);
  703. // 2秒后返回奔跑场景
  704. this.time.addEvent({
  705. delay: 2000,
  706. callback: () => {
  707. timeoutText.destroy();
  708. this.ball.destroy();
  709. this.resetToRunningScene();
  710. // 恢复计分定时器
  711. if (this.timer) {
  712. this.timer.paused = false;
  713. }
  714. },
  715. callbackScope: this
  716. });
  717. }
  718. shootBall() {
  719. if (!this.isShooting) return;
  720. // 清除超时计时器
  721. if (this.shootTimeout) {
  722. this.shootTimeout.remove();
  723. this.shootTimeout = null;
  724. }
  725. // 移除时间显示文本
  726. if (this.shootTimerText) {
  727. this.shootTimerText.destroy();
  728. this.shootTimerText = null;
  729. }
  730. // 禁用输入
  731. this.input.keyboard.off('keydown-SPACE', this.shootBall, this);
  732. this.input.off('pointerdown', this.shootBall, this);
  733. this.input.keyboard.off('keydown-LEFT');
  734. this.input.keyboard.off('keydown-RIGHT');
  735. if (this.shootHint) {
  736. this.shootHint.destroy();
  737. this.shootHint = null;
  738. }
  739. // 计算龙门网内的目标位置
  740. const goalNetY = this.goal.y + 30;
  741. const goalNetX = this.player.x;
  742. // 创建射门动画
  743. const ballTween = this.tweens.add({
  744. targets: this.ball,
  745. x: goalNetX,
  746. y: goalNetY,
  747. duration: 1000,
  748. ease: 'Power1',
  749. onComplete: () => {
  750. if (!this.ball || !this.ball.active) return;
  751. // 判断是否成功
  752. const isSuccess = Phaser.Math.Between(0, 1) === 1;
  753. if (isSuccess) {
  754. this.score += 3;
  755. this.updateScore();
  756. this.ball.setDepth(6.5); // 成功:足球在守门员后方(网内)
  757. this.successShoot();
  758. } else {
  759. this.ball.setDepth(7.5); // 失败:足球在守门员前方
  760. this.failShoot();
  761. }
  762. ballTween.remove();
  763. }
  764. });
  765. }
  766. // 射门成功处理
  767. successShoot() {
  768. this.isShooting = false;
  769. // 显示成功提示
  770. const successText = this.add.text(GAME_WIDTH / 2, GAME_HEIGHT / 2, '射门成功!+3分', {
  771. fontSize: '24px',
  772. fill: '#0f0',
  773. stroke: '#000000',
  774. strokeThickness: 2
  775. }).setOrigin(0.5);
  776. successText.setDepth(10);
  777. // 2秒后返回奔跑场景
  778. this.time.addEvent({
  779. delay: 2000,
  780. callback: () => {
  781. if (successText && successText.active) {
  782. successText.destroy();
  783. }
  784. this.ball.destroy();
  785. this.resetToRunningScene();
  786. // 恢复计分定时器
  787. if (this.timer) {
  788. this.timer.paused = false;
  789. }
  790. },
  791. callbackScope: this
  792. });
  793. }
  794. // 射门失败处理
  795. failShoot() {
  796. this.isShooting = false;
  797. // 关键修复1:解除足球的物理控制
  798. this.ball.setVelocity(0);
  799. this.ball.setImmovable(true);
  800. this.physics.world.disable(this.ball); // 完全禁用物理引擎对足球的控制
  801. // 关键修复2:调整足球位置到守门员前方固定位置,不跟随移动
  802. this.ball.setPosition(
  803. this.goalkeeper.x,
  804. this.goalkeeper.y + 20 // 固定在守门员前方20px处
  805. );
  806. // 显示失败提示
  807. const failText = this.add.text(GAME_WIDTH / 2, GAME_HEIGHT / 2, '射门失败!', {
  808. fontSize: '24px',
  809. fill: '#f00',
  810. stroke: '#000000',
  811. strokeThickness: 2
  812. }).setOrigin(0.5);
  813. failText.setDepth(10);
  814. // 2秒后返回奔跑场景
  815. this.time.addEvent({
  816. delay: 2000,
  817. callback: () => {
  818. if (failText && failText.active) {
  819. failText.destroy();
  820. }
  821. this.ball.destroy(); // 确保足球被销毁
  822. this.resetToRunningScene();
  823. // 恢复计分定时器
  824. if (this.timer) {
  825. this.timer.paused = false;
  826. }
  827. },
  828. callbackScope: this
  829. });
  830. }
  831. // 重置为奔跑场景
  832. resetToRunningScene() {
  833. // 确保清除超时计时器
  834. if (this.shootTimeout) {
  835. this.shootTimeout.remove();
  836. this.shootTimeout = null;
  837. }
  838. // 清除时间显示文本
  839. if (this.shootTimerText) {
  840. this.shootTimerText.destroy();
  841. this.shootTimerText = null;
  842. }
  843. this.clearScene();
  844. // 重置玩家状态
  845. if (this.player) {
  846. this.player.setTexture('playerAnim');
  847. this.player.setPosition(GAME_WIDTH / 2, GAME_HEIGHT - 100);
  848. this.player.setVelocity(0);
  849. }
  850. // 恢复背景和游戏状态
  851. if (this.background) this.background.setTexture('grass');
  852. this.gameActive = true;
  853. this.isShooting = false;
  854. // 确保足球被销毁
  855. if (this.ball) {
  856. this.ball.destroy();
  857. this.ball = null;
  858. }
  859. // 重新开始生成障碍物
  860. this.startSpawning();
  861. }
  862. gameOver() {
  863. this.gameActive = false;
  864. // 清理所有定时事件(增强版)
  865. // 1. 清除计分定时器
  866. if (this.timer) {
  867. this.timer.remove();
  868. this.timer.destroy(); // 彻底销毁计时器
  869. this.timer = null; // 置空引用
  870. }
  871. // 2. 清除障碍物生成计时器
  872. if (this.obstacleEvent) {
  873. this.obstacleEvent.remove();
  874. this.obstacleEvent.destroy();
  875. this.obstacleEvent = null;
  876. }
  877. // 3. 清除道具生成计时器
  878. if (this.jerseyEvent) {
  879. this.jerseyEvent.remove();
  880. this.jerseyEvent.destroy();
  881. this.jerseyEvent = null;
  882. }
  883. // if (this.broomEvent) {
  884. // this.broomEvent.remove();
  885. // this.broomEvent.destroy();
  886. // this.broomEvent = null;
  887. // }
  888. // 4. 清除所有可能残留的计时器
  889. this.time.removeAllEvents();
  890. // 5. 停止所有动画和物理运动
  891. this.physics.pause();
  892. // 显示游戏结束画面
  893. this.add.image(GAME_WIDTH / 2, GAME_HEIGHT / 2, 'gameOver').setOrigin(0.5);
  894. // 触发游戏结束事件
  895. this.time.addEvent({
  896. delay: 2000,
  897. callback: () => {
  898. this.game.events.emit('gameOver');
  899. },
  900. loop: false // 确保这是一次性事件
  901. });
  902. }
  903. update() {
  904. if (!this.gameActive) return;
  905. // 关键修复:射门模式下直接返回,不执行任何移动逻辑
  906. if (this.isShooting) return;
  907. // 玩家移动逻辑(原有代码保持不变)
  908. if (this.player && this.player.active) {
  909. if (this.cursors.left.isDown) {
  910. //this.player.setVelocityX(-200);
  911. this.player.setX(direction.value + 46);
  912. if (this.player.anims.currentAnim?.key !== 'playerLeft' && !this.player.anims.paused) {
  913. this.player.anims.play('playerLeft', true);
  914. }
  915. } else if (this.cursors.right.isDown) {
  916. //this.player.setVelocityX(200);
  917. this.player.setX(direction.value + 46);
  918. if (this.player.anims.currentAnim?.key !== 'playerRight' && !this.player.anims.paused) {
  919. this.player.anims.play('playerRight', true);
  920. }
  921. } else if (this.cursors.up.isDown) {
  922. this.player.setVelocityY(-200);
  923. if (this.player.anims.currentAnim?.key === 'playerLeft' && !this.player.anims.paused) {
  924. this.player.anims.play('playerLeft', true);
  925. } else if (this.player.anims.currentAnim?.key === 'playerRight' && !this.player.anims.paused) {
  926. this.player.anims.play('playerRight', true);
  927. } else {
  928. this.player.anims.play('playerLeft', true);
  929. }
  930. } else if (this.cursors.down.isDown) {
  931. this.player.setVelocityY(200);
  932. if (this.player.anims.currentAnim?.key === 'playerLeft' && !this.player.anims.paused) {
  933. this.player.anims.play('playerLeft', true);
  934. } else if (this.player.anims.currentAnim?.key === 'playerRight' && !this.player.anims.paused) {
  935. this.player.anims.play('playerRight', true);
  936. } else {
  937. this.player.anims.play('playerLeft', true);
  938. }
  939. } else {
  940. this.player.setVelocityX(0);
  941. this.player.setVelocityY(0);
  942. this.player.anims.stop();
  943. }
  944. }
  945. }
  946. // 添加 clearScene 方法
  947. clearScene() {
  948. // 清理射门场景的元素
  949. if (this.goal) this.goal.destroy();
  950. if (this.goalkeeper) this.goalkeeper.destroy();
  951. if (this.ball) {
  952. this.ball.destroy();
  953. this.ball = null; // 显式置空
  954. }
  955. if (this.shootHint) this.shootHint.destroy();
  956. // 清理所有障碍物和道具
  957. this.obstacles.forEach(obs => obs.destroy());
  958. this.powerUps.forEach(p => p.destroy());
  959. this.obstacles = [];
  960. this.powerUps = [];
  961. // 停止所有动画
  962. this.tweens.killAll();
  963. }
  964. // 新增:场景销毁时清理资源
  965. destroy() {
  966. // 清理所有定时器
  967. if (this.timer) this.timer.remove();
  968. if (this.obstacleEvent) this.obstacleEvent.remove();
  969. if (this.jerseyEvent) this.jerseyEvent.remove();
  970. // if (this.broomEvent) this.broomEvent.remove();
  971. // 移除输入监听
  972. this.input.keyboard.off('keydown-SPACE', this.shootBall, this);
  973. this.input.off('pointerdown', this.shootBall, this);
  974. // 清理所有游戏对象
  975. this.obstacles.forEach(obs => obs.destroy());
  976. this.powerUps.forEach(p => p.destroy());
  977. if (this.goal) this.goal.destroy();
  978. if (this.goalkeeper) this.goalkeeper.destroy();
  979. if (this.ball) this.ball.destroy();
  980. // 调用父类销毁方法
  981. super.destroy();
  982. }
  983. }
  984. // 游戏控制函数
  985. const startGame = () => {
  986. currentScene.value = 'countdown';
  987. // 倒计时
  988. currentScene.value = 'countdown';
  989. let count = 3;
  990. const countdownInterval = setInterval(() => {
  991. count--;
  992. countdownNum.value = count;
  993. if (count <= 0) {
  994. clearInterval(countdownInterval);
  995. currentScene.value = 'game';
  996. // 通知游戏开始
  997. if (game.value) {
  998. const gameScene = game.value.scene.getScene('GameScene');
  999. if (gameScene) {
  1000. gameScene.startGame();
  1001. }
  1002. }
  1003. }
  1004. }, 1000);
  1005. };
  1006. const continueGame = () => {
  1007. startGame();
  1008. };
  1009. const restartGame = () => {
  1010. // 1. 彻底销毁旧游戏实例
  1011. if (game.value) {
  1012. // 获取当前场景并停止所有活动
  1013. const gameScene = game.value.scene.getScene('GameScene');
  1014. if (gameScene) {
  1015. gameScene.gameActive = false;
  1016. gameScene.time.removeAllEvents(); // 清除所有事件
  1017. gameScene.physics.pause(); // 暂停物理引擎
  1018. }
  1019. // 销毁游戏实例
  1020. game.value.destroy(true);
  1021. game.value = null;
  1022. }
  1023. // 2. 重置全局状态
  1024. score.value = 0;
  1025. currentScene.value = 'start';
  1026. countdownNum.value = 3;
  1027. // 3. 初始化新游戏
  1028. setTimeout(() => {
  1029. initGame();
  1030. }, 100);
  1031. };
  1032. const getTitle = (score) => {
  1033. if (score < 50) return '足球新手';
  1034. if (score < 100) return '业余球员';
  1035. if (score < 200) return '专业选手';
  1036. if (score < 300) return '足球明星';
  1037. return '足球传奇';
  1038. };
  1039. // 组件挂载时初始化游戏
  1040. onMounted(() => {
  1041. initGame();
  1042. });
  1043. // 监听场景变化
  1044. watch(currentScene, (newVal) => {
  1045. if (newVal === 'game' && game.value) {
  1046. const gameScene = game.value.scene.getScene('GameScene');
  1047. if (gameScene) {
  1048. gameScene.scene.resume();
  1049. }
  1050. }
  1051. });
  1052. /**
  1053. * 监听投篮
  1054. */
  1055. watch(
  1056. () => myThrow.value,
  1057. (newData, oldData) => {
  1058. if (oldData == undefined || myTimer.value) {
  1059. return false;
  1060. }
  1061. console.log("ppp", oldData, newData)
  1062. if (newData == 1) {
  1063. console.log("收腿准备")
  1064. } else {
  1065. if (currentScene.value === 'start') {
  1066. startGame();
  1067. }
  1068. const gameScene = game.value.scene.getScene('GameScene');
  1069. gameScene.shootBall();
  1070. console.log("踢出去了")
  1071. //加个时间以免踢出去然后收腿也算了
  1072. myTimer.value = setTimeout(() => {
  1073. clearTimeout(myTimer.value);
  1074. myTimer.value = null;
  1075. }, 500)
  1076. }
  1077. },
  1078. { immediate: true }
  1079. );
  1080. /**
  1081. * 监听跑动
  1082. */
  1083. watch(
  1084. () => direction.value,
  1085. (newData, oldData) => {
  1086. nextTick(() => {
  1087. if (!game.value) {
  1088. return false;
  1089. }
  1090. const gameScene = game.value.scene.getScene('GameScene');
  1091. if (newData > oldData) {
  1092. gameScene.cursors.left.isDown = false;
  1093. gameScene.cursors.right.isDown = true;
  1094. } else {
  1095. gameScene.cursors.left.isDown = true;
  1096. gameScene.cursors.right.isDown = false;
  1097. }
  1098. });
  1099. },
  1100. { immediate: true }
  1101. );
  1102. /**
  1103. * 初始化
  1104. */
  1105. const getInit = async () => {
  1106. console.log("触发姿态识别")
  1107. let deviceid = localStorage.getItem('deviceid') || '';
  1108. if (!deviceid) {
  1109. proxy?.$modal.msgError(`请重新登录绑定设备号后使用`);
  1110. return false;
  1111. }
  1112. bodyposeState.value = true;
  1113. if (wsState.value) {
  1114. proxy?.$modal.msgWarning(`操作过快,请稍后重试`);
  1115. setTimeout(() => {
  1116. bodyposeState.value = false;
  1117. }, 1000)
  1118. return false;
  1119. }
  1120. speckText("正在姿态识别");
  1121. proxy?.$modal.msgWarning(`正在姿态识别`);
  1122. bodyposeWs((e: any) => {
  1123. //console.log("bodyposeWS", e)
  1124. if (e?.wksid) {
  1125. wsState.value = true;
  1126. //获取设备信息
  1127. startDevice({ deviceid: deviceid });
  1128. console.log("获取设备信息")
  1129. }
  1130. if (e?.type == 'fe_device_init_result') {
  1131. //接收设备信息并发送请求
  1132. if (e?.device_info) {
  1133. deviceInfo.value = e.device_info;
  1134. getCheckBodypose();
  1135. console.log("返回设备信息,检查是否支持姿态识别")
  1136. } else {
  1137. proxy?.$modal.msgError(`设备信息缺失,请重新登录绑定设备号后使用`);
  1138. }
  1139. }
  1140. if (e?.cmd == 'check_bodyposecontroller_available') {
  1141. let handcontroller_id = deviceInfo.value.handcontroller_id;
  1142. if (e?.code == 0) {
  1143. //查看姿态识别状态,如果不处于关闭就先关闭再重新启动(可能会APP退出然后工作站还在运行的可能性)
  1144. getBodyposeState(handcontroller_id);
  1145. againNum.value = 0;
  1146. againTimer.value = null;
  1147. clearTimeout(againTimer.value);
  1148. console.log("查看姿态识别状态")
  1149. } else {
  1150. //尝试多次查询姿态识别状态
  1151. if (againNum.value <= 2) {
  1152. againTimer.value = setTimeout(() => {
  1153. getCheckBodypose();
  1154. }, 500)
  1155. againNum.value++;
  1156. } else {
  1157. let msg = "";
  1158. if (e.code == 102402) {
  1159. msg = `多次连接失败请重试,姿态识别模块被占用`;
  1160. } else {
  1161. msg = `多次连接失败请重试,姿态识别模块不可用,code:${e.code}`;
  1162. }
  1163. proxy?.$modal.msgWarning(msg);
  1164. againNum.value = 0;
  1165. againTimer.value = null;
  1166. clearTimeout(againTimer.value);
  1167. getCloseBodypose();//直接关闭
  1168. }
  1169. }
  1170. }
  1171. if (e?.cmd == 'get_bodyposecontroller_state') {
  1172. let handcontroller_id = deviceInfo.value.handcontroller_id;
  1173. //state说明: 0:关闭,3:空闲,36:工作中
  1174. if ([3, 36].includes(e.state)) {
  1175. getCloseBodypose();
  1176. proxy?.$modal.msgWarning(`请重新姿态识别`);
  1177. } else {
  1178. openBodypose(handcontroller_id);
  1179. }
  1180. }
  1181. if (e?.type == 'bodyposecontroller_result') {
  1182. let arr = e.data.result.keypoints;
  1183. let result = [];
  1184. for (let i = 0; i < arr.length; i += 3) {
  1185. result.push(arr.slice(i, i + 2));
  1186. }
  1187. //console.log("result", result)
  1188. bodyposeData.value = result;
  1189. if (boxes.value.length == 0) {
  1190. speckText("识别成功");
  1191. proxy?.$modal.msgWarning(`识别成功`);
  1192. let arr = e.data.result.boxes;
  1193. 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] }]
  1194. proportion.value = (clientObj.value.height / (arr[3] - arr[1])).toFixed(2);
  1195. }
  1196. getCanvas();
  1197. }
  1198. if (e?.cmd == 'terminate_bodyposecontroller') {
  1199. if (e?.code == 0) {
  1200. closeWS();
  1201. bodyposeState.value = false;
  1202. }
  1203. }
  1204. if (e?.type == 'disconnect') {
  1205. wsState.value = false;
  1206. }
  1207. });
  1208. };
  1209. /**
  1210. * 查询姿态识别状态
  1211. */
  1212. const getCheckBodypose = () => {
  1213. let handcontroller_id = deviceInfo.value.handcontroller_id;
  1214. //检查是否支持姿态识别
  1215. checkBodypose(handcontroller_id);
  1216. };
  1217. /**
  1218. * 关闭姿态识别
  1219. */
  1220. const getCloseBodypose = () => {
  1221. let handcontroller_id = deviceInfo.value.handcontroller_id;
  1222. terminateBodypose(handcontroller_id);
  1223. bodyposeState.value = false;
  1224. speckCancel(); //停止播报
  1225. setTimeout(() => {
  1226. if (wsState.value) {
  1227. closeWS();
  1228. }
  1229. }, 3000)
  1230. };
  1231. const getCanvas = () => {
  1232. let leftA = { x: bodyposeData.value[12][0], y: bodyposeData.value[12][1] };//大腿
  1233. let leftB = { x: bodyposeData.value[14][0], y: bodyposeData.value[14][1] };//膝盖
  1234. let leftC = { x: bodyposeData.value[16][0], y: bodyposeData.value[16][1] };//脚
  1235. let rightA = { x: bodyposeData.value[11][0], y: bodyposeData.value[11][1] };//大腿
  1236. let rightB = { x: bodyposeData.value[13][0], y: bodyposeData.value[13][1] };//膝盖
  1237. let rightC = { x: bodyposeData.value[15][0], y: bodyposeData.value[15][1] };//脚
  1238. let jiaodu1 = calculateAngleAtB(leftA, leftB, leftC)
  1239. let jiaodu2 = calculateAngleAtB(rightA, rightB, rightC)
  1240. // console.log("jiaodu1",jiaodu1)
  1241. // console.log("jiaodu2",jiaodu2)
  1242. if (jiaodu1 <= 80 && jiaodu2 >= 120 || jiaodu2 <= 80 && jiaodu2 >= 120) {
  1243. myThrow.value = 1;
  1244. } else {
  1245. myThrow.value = 0;
  1246. }
  1247. const canvas: any = canvasRef.value;
  1248. const ctx = canvas.getContext('2d');
  1249. // 清空整个画布
  1250. ctx.clearRect(0, 0, canvas.width, canvas.height);
  1251. // 保存当前状态
  1252. ctx.save();
  1253. function calculateOffset(a: any, b: any) {
  1254. return {
  1255. x: b.x - a.x,
  1256. y: b.y - a.y
  1257. };
  1258. }
  1259. const pointA = { x: clientObj.value.width / 2, y: clientObj.value.height / 2 };
  1260. const pointB = { x: (boxes.value[2].x + boxes.value[0].x) / 2, y: (boxes.value[3].y + boxes.value[1].y) / 2 };
  1261. const offset = calculateOffset(pointA, pointB);
  1262. ctx.translate(-offset.x, -offset.y);
  1263. // console.log("Canvas分辨率", clientObj.value);
  1264. // console.log("人体图片四点坐标", boxes.value)
  1265. // console.log("Canvas中心", pointA);
  1266. // console.log("人体中心", pointB);
  1267. // console.log("offset", offset)
  1268. // console.log("proportion.value",proportion.value)
  1269. const originalPoints = bodyposeData.value;
  1270. // 计算缩放后坐标
  1271. const postData = originalPoints.map((point: any) => {
  1272. const newX = (point[0] - pointB.x) * proportion.value + pointB.x;
  1273. const newY = (point[1] - pointB.y) * proportion.value + pointB.y;
  1274. return [newX, newY];
  1275. });
  1276. // console.log("原始坐标:", originalPoints);
  1277. // console.log("缩放后坐标:", postData);
  1278. direction.value = postData[0][0] - offset.x - (94 / 2);//鼻子X
  1279. //绘制头部
  1280. const point1 = { x: postData[4][0], y: postData[4][1] };
  1281. const point2 = { x: postData[3][0], y: postData[3][1] };
  1282. // 计算椭圆参数
  1283. const centerX = (point1.x + point2.x) / 2; // 椭圆中心X
  1284. const centerY = (point1.y + point2.y) / 2; // 椭圆中心Y
  1285. const distance = Math.sqrt(
  1286. Math.pow(point2.x - point1.x, 2) +
  1287. Math.pow(point2.y - point1.y, 2)
  1288. ); // 两个焦点之间的距离
  1289. const radiusX = distance * 0.5; // 水平半径(可调整)
  1290. const radiusY = distance * 0.6; // 垂直半径(可调整)
  1291. // 1. 绘制填充椭圆
  1292. ctx.beginPath();
  1293. ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2);
  1294. ctx.fillStyle = 'red'; // 填充颜色
  1295. ctx.fill(); // 填充
  1296. // 2. 绘制边框
  1297. ctx.strokeStyle = 'red';
  1298. ctx.lineWidth = 5;
  1299. ctx.stroke();
  1300. // 绘制每个点
  1301. postData.forEach((point: any, index: number) => {
  1302. //眼睛鼻子不显示
  1303. if (![0, 1, 2].includes(index)) {
  1304. const [x, y] = point;
  1305. ctx.beginPath();
  1306. ctx.arc(x, y, 5, 0, Math.PI * 2); // 绘制半径为5的圆点
  1307. ctx.fillStyle = 'red';
  1308. ctx.fill();
  1309. ctx.lineWidth = 1;
  1310. ctx.stroke();
  1311. }
  1312. });
  1313. // 根据点关系连线
  1314. 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]]
  1315. arr.forEach((point: any) => {
  1316. let index1 = point[0];
  1317. let index2 = point[1];
  1318. //连线
  1319. const dian1 = { x: postData[index1][0], y: postData[index1][1] };
  1320. const dian2 = { x: postData[index2][0], y: postData[index2][1] };
  1321. // 绘制连线
  1322. ctx.beginPath();
  1323. ctx.moveTo(dian1.x, dian1.y); // 起点
  1324. ctx.lineTo(dian2.x, dian2.y); // 终点
  1325. ctx.strokeStyle = 'red'; // 线条颜色
  1326. ctx.lineWidth = 3; // 线条宽度
  1327. ctx.stroke(); // 描边
  1328. });
  1329. ctx.restore(); // 恢复状态
  1330. };
  1331. /**
  1332. * 计算B点的夹角度数(∠ABC)
  1333. * @param {Object} pointA - A点坐标 {x, y, z可选}
  1334. * @param {Object} pointB - B点坐标 {x, y, z可选}
  1335. * @param {Object} pointC - C点坐标 {x, y, z可选}
  1336. * @returns {number} B点的夹角度数(保留两位小数)
  1337. */
  1338. function calculateAngleAtB(pointA, pointB, pointC) {
  1339. // 计算向量BA和向量BC
  1340. const vectorBA = {
  1341. x: pointA.x - pointB.x,
  1342. y: pointA.y - pointB.y,
  1343. z: (pointA.z || 0) - (pointB.z || 0)
  1344. };
  1345. const vectorBC = {
  1346. x: pointC.x - pointB.x,
  1347. y: pointC.y - pointB.y,
  1348. z: (pointC.z || 0) - (pointB.z || 0)
  1349. };
  1350. // 计算点积
  1351. const dotProduct =
  1352. vectorBA.x * vectorBC.x +
  1353. vectorBA.y * vectorBC.y +
  1354. vectorBA.z * vectorBC.z;
  1355. // 计算向量BA的模长
  1356. const lengthBA = Math.sqrt(
  1357. vectorBA.x ** 2 +
  1358. vectorBA.y ** 2 +
  1359. vectorBA.z ** 2
  1360. );
  1361. // 计算向量BC的模长
  1362. const lengthBC = Math.sqrt(
  1363. vectorBC.x ** 2 +
  1364. vectorBC.y ** 2 +
  1365. vectorBC.z ** 2
  1366. );
  1367. // 防止除以零的情况
  1368. if (lengthBA === 0 || lengthBC === 0) {
  1369. throw new Error("点A、B、C不能重合");
  1370. }
  1371. // 计算余弦值
  1372. const cosine = dotProduct / (lengthBA * lengthBC);
  1373. // 由于计算误差可能导致cosine略超出[-1, 1]范围,需要修正
  1374. const clampedCosine = Math.max(Math.min(cosine, 1), -1);
  1375. // 计算弧度并转换为角度
  1376. const angleRadians = Math.acos(clampedCosine);
  1377. const angleDegrees = angleRadians * (180 / Math.PI);
  1378. // 保留两位小数并返回
  1379. return parseFloat(angleDegrees.toFixed(2));
  1380. }
  1381. onBeforeMount(() => {
  1382. clientObj.value = {
  1383. width: document.documentElement.clientWidth,
  1384. height: document.documentElement.clientHeight,
  1385. }
  1386. getInit();
  1387. });
  1388. // 组件卸载时清理事件监听
  1389. onUnmounted(() => {
  1390. if (game.value && game.value.events && scoreListener) {
  1391. game.value.events.off('scoreChanged', scoreListener);
  1392. }
  1393. });
  1394. </script>
  1395. <style scoped>
  1396. .game-container {
  1397. position: relative;
  1398. width: 100%;
  1399. height: 100vh;
  1400. margin: 0 auto;
  1401. overflow: hidden;
  1402. }
  1403. .game-canvas {
  1404. width: 100%;
  1405. height: 100%;
  1406. background-color: #000;
  1407. }
  1408. .gamestart {
  1409. position: absolute;
  1410. top: 0;
  1411. left: 0;
  1412. width: 100%;
  1413. height: 100%;
  1414. z-index: 100;
  1415. }
  1416. .start_bg {
  1417. width: 100%;
  1418. height: 100%;
  1419. object-fit: cover;
  1420. }
  1421. .btn {
  1422. width: 80%;
  1423. height: 40px;
  1424. border-radius: 5px;
  1425. text-align: center;
  1426. line-height: 40px;
  1427. color: #333;
  1428. box-shadow: 0 5px 0px #07942c;
  1429. box-shadow: 0 5px 0px rgba(0, 0, 0, 0.17);
  1430. position: absolute;
  1431. left: 50%;
  1432. margin: 0 0 0 -40%;
  1433. font-size: 18px;
  1434. }
  1435. .rule {
  1436. background: #fff;
  1437. bottom: 100px;
  1438. }
  1439. .start {
  1440. background: #ffff00;
  1441. bottom: 40px;
  1442. }
  1443. .ruleshadow {
  1444. position: absolute;
  1445. top: 0;
  1446. left: 0;
  1447. width: 100%;
  1448. height: 100%;
  1449. background-color: rgba(0, 0, 0, 0.7);
  1450. z-index: 102;
  1451. display: flex;
  1452. justify-content: center;
  1453. align-items: center;
  1454. font-size: 18px;
  1455. }
  1456. .rulebox {
  1457. width: 280px;
  1458. background-color: white;
  1459. border-radius: 10px;
  1460. padding: 20px;
  1461. position: relative;
  1462. opacity: 0;
  1463. animation: fadeIn 0.5s forwards;
  1464. }
  1465. @keyframes fadeIn {
  1466. to {
  1467. opacity: 1;
  1468. }
  1469. }
  1470. .x_rulebox {
  1471. position: absolute;
  1472. top: 10px;
  1473. right: 10px;
  1474. width: 20px;
  1475. height: 20px;
  1476. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23000'%3E%3Cpath d='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3C/svg%3E");
  1477. cursor: pointer;
  1478. }
  1479. .daoju {
  1480. margin-top: 15px;
  1481. }
  1482. .daoju_item {
  1483. display: flex;
  1484. align-items: center;
  1485. margin-bottom: 10px;
  1486. }
  1487. .daoju_1,
  1488. .daoju_2,
  1489. .daoju_3 {
  1490. display: inline-block;
  1491. width: 60px;
  1492. height: 60px;
  1493. margin-right: 10px;
  1494. background-size: contain;
  1495. background-repeat: no-repeat;
  1496. flex-shrink: 0;
  1497. }
  1498. .daoju_1 {
  1499. background-image: url("/static/images/football/Jersey.png");
  1500. }
  1501. .daoju_2 {
  1502. background-image: url("/static/images/football/Broom.png");
  1503. }
  1504. .daoju_3 {
  1505. background-image: url("/static/images/football/Pile.png");
  1506. }
  1507. .zhiyin {
  1508. position: absolute;
  1509. top: 0;
  1510. left: 0;
  1511. width: 100%;
  1512. height: 100%;
  1513. background-color: rgba(0, 0, 0, 0.7);
  1514. z-index: 100;
  1515. display: flex;
  1516. justify-content: center;
  1517. align-items: center;
  1518. }
  1519. .daojishi {
  1520. position: absolute;
  1521. width: 100%;
  1522. top: 0;
  1523. bottom: 0;
  1524. display: none;
  1525. z-index: 11;
  1526. background: url(daojishu_bg.png) repeat left top;
  1527. background-size: 100%;
  1528. }
  1529. .daojishiline {
  1530. width: 100%;
  1531. height: 95px;
  1532. position: absolute;
  1533. top: 109px;
  1534. left: 0;
  1535. background: #ffe400;
  1536. z-index: 1;
  1537. }
  1538. .daojishibox {
  1539. width: 180px;
  1540. height: 180px;
  1541. position: absolute;
  1542. left: 50%;
  1543. top: 71px;
  1544. margin: 0 0 0 -90px;
  1545. background: url(daojishibox.png) no-repeat left top;
  1546. background-size: 100%;
  1547. z-index: 2;
  1548. }
  1549. .daojishipangzi {
  1550. width: 111px;
  1551. height: 102px;
  1552. position: absolute;
  1553. left: 50%;
  1554. bottom: 50px;
  1555. margin: 0 0 0 -55px;
  1556. background: url(daojishipangzi.png) no-repeat left top;
  1557. background-size: 100%;
  1558. z-index: -1;
  1559. }
  1560. .daojishinum {
  1561. width: 100%;
  1562. text-align: center;
  1563. font-size: 80px;
  1564. color: #FFFFFF;
  1565. padding: 70px 0 0 0;
  1566. line-height: normal;
  1567. }
  1568. @keyframes countDown {
  1569. 0% {
  1570. transform: scale(3);
  1571. opacity: 0;
  1572. }
  1573. 50% {
  1574. transform: scale(1.2);
  1575. opacity: 1;
  1576. }
  1577. 100% {
  1578. transform: scale(1);
  1579. opacity: 1;
  1580. }
  1581. }
  1582. .gameend {
  1583. position: absolute;
  1584. top: 0;
  1585. left: 0;
  1586. width: 100%;
  1587. height: 100%;
  1588. background-color: rgba(0, 0, 0, 0.7);
  1589. z-index: 100;
  1590. display: flex;
  1591. flex-direction: column;
  1592. justify-content: center;
  1593. align-items: center;
  1594. color: white;
  1595. }
  1596. .game_fenshu {
  1597. font-size: 36px;
  1598. margin-bottom: 20px;
  1599. }
  1600. .chenghao {
  1601. font-size: 24px;
  1602. margin-bottom: 30px;
  1603. text-align: center;
  1604. }
  1605. .game_ogain,
  1606. .game_chakan,
  1607. .game_share {
  1608. width: 150px;
  1609. height: 40px;
  1610. line-height: 40px;
  1611. text-align: center;
  1612. background-color: #f00;
  1613. border-radius: 20px;
  1614. margin-bottom: 10px;
  1615. cursor: pointer;
  1616. font-size: 18px;
  1617. }
  1618. .game_chakan {
  1619. background-color: #666;
  1620. }
  1621. .game_share {
  1622. background-color: #008000;
  1623. }
  1624. </style>