football.vue 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302
  1. <template>
  2. <div class="game-container">
  3. <div id="gameCanvas" class="game-canvas"></div>
  4. <!-- 游戏启动界面 -->
  5. <div v-if="currentScene === 'start'" class="gamestart">
  6. <img v-if="currentScene === 'start'" src="/static/images/football/game_start.jpg" class="start_bg" />
  7. <div class="btn rule" @click="showRules = true">查看规则</div>
  8. <div class="btn start" @click="startGame">开始游戏</div>
  9. <!-- 规则弹窗 -->
  10. <div v-if="showRules" class="ruleshadow">
  11. <div class="rulebox">
  12. <span class="x_rulebox" @click="showRules = false"></span>
  13. <h3>操作方法:</h3>
  14. <p>手指拖动控制人物带球奔跑的方向和速度。</p>
  15. <h3>道具说明:</h3>
  16. <div class="daoju">
  17. <div class="daoju_item">
  18. <span class="daoju_1"></span>
  19. <p>每收集一个,生命值+1;触碰障碍物,生命可再复活。</p>
  20. </div>
  21. <div class="daoju_item">
  22. <span class="daoju_2"></span>
  23. <p>每收集一个,能力值+1;扫除面前一切障碍,加速前进。</p>
  24. </div>
  25. <div class="daoju_item">
  26. <span class="daoju_3"></span>
  27. <p>触碰锥桶,生命值-1;成功绕过一次分数+1。</p>
  28. </div>
  29. </div>
  30. </div>
  31. </div>
  32. </div>
  33. <!-- 规则说明 -->
  34. <div v-if="currentScene === 'rule'" class="zhiyin">
  35. <div class="btn start" @click="continueGame">继续游戏</div>
  36. </div>
  37. <!-- 倒计时 -->
  38. <div v-if="currentScene === 'countdown'" class="daoju">
  39. <div class="daojishibox">
  40. <div class="daojishinum">{{ countdownNum }}</div>
  41. </div>
  42. </div>
  43. <!-- 游戏结束 -->
  44. <div v-if="currentScene === 'gameover'" class="gameend">
  45. <div v-if="currentScene === 'gameover'" class="game_fenshu">
  46. <span>{{ score }}</span>分
  47. </div>
  48. <div class="chenghao">恭喜你获得称号<p>{{ getTitle(score) }}</p>
  49. </div>
  50. <div class="game_ogain" @click="restartGame">再次游戏</div>
  51. </div>
  52. </div>
  53. </template>
  54. <script setup>
  55. import { ref, onMounted, watch } from 'vue';
  56. import Phaser from 'phaser';
  57. // 游戏状态管理
  58. const currentScene = ref('start');
  59. const showRules = ref(false);
  60. const countdownNum = ref(3);
  61. const score = ref(0);
  62. const game = ref(null);
  63. const gameConfig = ref(null);
  64. let scoreListener = null; // 存储事件监听器引用
  65. // 游戏常量
  66. const width = document.documentElement.clientWidth;
  67. const height = document.documentElement.clientHeight;
  68. const GAME_WIDTH = width;
  69. const GAME_HEIGHT = height;
  70. // 游戏资源
  71. const gameAssets = {
  72. images: [
  73. { key: 'gameStart', url: 'static/images/football/game_start.jpg' },
  74. { key: 'grass', url: 'static/images/football/Caopi.png' },
  75. { key: 'playerShoot', url: 'static/images/football/QiuyuanShooting.png' },
  76. { key: 'pile', url: 'static/images/football/Pile.png' },
  77. { key: 'jersey', url: 'static/images/football/Jersey.png' },
  78. { key: 'broom', url: 'static/images/football/Broom.png' },
  79. { key: 'goalkeeper', url: 'static/images/football/Goalkeeper.png' },
  80. { key: 'goal', url: 'static/images/football/Goal.png' },
  81. { key: 'ball', url: 'static/images/football/Ball.png' },
  82. { key: 'line', url: 'static/images/football/Line.png' },
  83. { key: 'line2', url: 'static/images/football/Line2.png' },
  84. { key: 'goalBackground', url: 'static/images/football/GoalBackGround.png' },
  85. { key: 'gameOver', url: 'static/images/football/gameover.png' }
  86. ],
  87. spritesheets: [
  88. {
  89. key: 'playerAnim',
  90. url: 'static/images/football/Qiuyuan.png',
  91. frameWidth: 92.5,
  92. frameHeight: 92.5
  93. },
  94. {
  95. key: 'ballAnim',
  96. url: 'static/images/football/Ball.png',
  97. frameWidth: 31,
  98. frameHeight: 31
  99. },
  100. {
  101. key: 'goalkeeperAnim',
  102. url: 'static/images/football/Goalkeeper.png',
  103. frameWidth: 55,
  104. frameHeight: 56
  105. },
  106. {
  107. key: 'playerCollision',
  108. url: 'static/images/football/QiuyuanCollision.png',
  109. frameWidth: 92.5,
  110. frameHeight: 92.5
  111. },
  112. {
  113. key: 'playerSuper',
  114. url: 'static/images/football/QiuyuanSuper.png',
  115. frameWidth: 92.5,
  116. frameHeight: 92.5
  117. },
  118. ]
  119. };
  120. // 初始化Phaser游戏
  121. const initGame = () => {
  122. // 游戏配置
  123. gameConfig.value = {
  124. type: Phaser.AUTO,
  125. width: GAME_WIDTH,
  126. height: GAME_HEIGHT,
  127. parent: 'gameCanvas',
  128. physics: {
  129. default: 'arcade',
  130. arcade: {
  131. gravity: { y: 0 },
  132. debug: false
  133. }
  134. },
  135. scene: [
  136. PreloaderScene,
  137. GameScene
  138. ]
  139. };
  140. game.value = new Phaser.Game(gameConfig.value);
  141. // 关键:使用局部变量存储事件监听器,便于后续清理
  142. scoreListener = (newScore) => {
  143. score.value = newScore;
  144. };
  145. // 添加事件监听
  146. if (game.value && game.value.events) {
  147. game.value.events.on('scoreChanged', scoreListener);
  148. }
  149. game.value.events.on('gameOver', () => {
  150. currentScene.value = 'gameover';
  151. });
  152. };
  153. // 预加载场景
  154. class PreloaderScene extends Phaser.Scene {
  155. constructor() {
  156. super('PreloaderScene');
  157. }
  158. preload() {
  159. // 加载图片资源
  160. gameAssets.images.forEach(asset => {
  161. this.load.image(asset.key, asset.url);
  162. });
  163. // 加载精灵表
  164. gameAssets.spritesheets.forEach(asset => {
  165. this.load.spritesheet(asset.key, asset.url, {
  166. frameWidth: asset.frameWidth,
  167. frameHeight: asset.frameHeight
  168. });
  169. });
  170. // 显示加载进度
  171. const progressBar = this.add.graphics();
  172. const progressBox = this.add.graphics();
  173. progressBox.fillStyle(0x222222, 0.8);
  174. progressBox.fillRect(GAME_WIDTH / 2 - 160, GAME_HEIGHT / 2 - 25, 320, 50);
  175. const loadingText = this.make.text({
  176. x: GAME_WIDTH / 2,
  177. y: GAME_HEIGHT / 2 - 50,
  178. text: 'Loading...',
  179. style: {
  180. font: '20px monospace',
  181. fill: '#ffffff'
  182. }
  183. }).setOrigin(0.5, 0.5);
  184. const percentText = this.make.text({
  185. x: GAME_WIDTH / 2,
  186. y: GAME_HEIGHT / 2,
  187. text: '0%',
  188. style: {
  189. font: '18px monospace',
  190. fill: '#ffffff'
  191. }
  192. }).setOrigin(0.5, 0.5);
  193. this.load.on('progress', (value) => {
  194. percentText.setText(`${Math.round(value * 100)}%`);
  195. progressBar.clear();
  196. progressBar.fillStyle(0xffffff, 1);
  197. progressBar.fillRect(GAME_WIDTH / 2 - 150, GAME_HEIGHT / 2 - 15, 300 * value, 30);
  198. });
  199. this.load.on('complete', () => {
  200. progressBar.destroy();
  201. progressBox.destroy();
  202. loadingText.destroy();
  203. percentText.destroy();
  204. });
  205. }
  206. create() {
  207. // 初始化动画
  208. this.initAnimations();
  209. // 切换到游戏场景
  210. this.scene.start('GameScene');
  211. }
  212. initAnimations() {
  213. // 球员动画
  214. this.anims.create({
  215. key: 'playerLeft',
  216. frames: [
  217. { key: 'playerAnim', frame: 0 },
  218. { key: 'playerAnim', frame: 2 },
  219. { key: 'playerAnim', frame: 4 },
  220. { key: 'playerAnim', frame: 6 }
  221. ],
  222. frameRate: 10,
  223. repeat: -1
  224. });
  225. this.anims.create({
  226. key: 'playerRight',
  227. frames: [
  228. { key: 'playerAnim', frame: 1 },
  229. { key: 'playerAnim', frame: 3 },
  230. { key: 'playerAnim', frame: 5 },
  231. { key: 'playerAnim', frame: 7 }
  232. ],
  233. frameRate: 10,
  234. repeat: -1
  235. });
  236. // 足球动画
  237. this.anims.create({
  238. key: 'ballAnim',
  239. frames: this.anims.generateFrameNumbers('ballAnim', { start: 0, end: 1 }),
  240. frameRate: 5,
  241. repeat: -1
  242. });
  243. // 守门员动画
  244. this.anims.create({
  245. key: 'goalkeeperAnim',
  246. frames: this.anims.generateFrameNumbers('goalkeeperAnim', { start: 0, end: 1 }),
  247. frameRate: 3,
  248. repeat: -1
  249. });
  250. }
  251. }
  252. // 游戏主场景
  253. class GameScene extends Phaser.Scene {
  254. constructor() {
  255. super('GameScene');
  256. this.player = null;
  257. this.score = 0;
  258. this.lives = 3;
  259. this.maxLives = 5;
  260. this.speed = 6;
  261. this.acceleration = 1.9;
  262. this.level = 1;
  263. this.obstacles = [];
  264. this.powerUps = [];
  265. this.isSuper = false;
  266. this.gameActive = false;
  267. this.timer = null;
  268. this.isShooting = false; // 新增:初始化射门状态
  269. this.obstacleTimer = null; // 新增:初始化障碍物计时器
  270. this.obstacleEvent = null;
  271. this.jerseyEvent = null;
  272. this.broomEvent = null;
  273. }
  274. create() {
  275. // 创建背景
  276. this.createBackground();
  277. // 创建玩家
  278. this.createPlayer();
  279. // 创建计分板
  280. this.createHUD();
  281. // 输入控制
  282. this.initControls();
  283. // 游戏事件
  284. this.events.on('resume', () => {
  285. this.gameActive = true;
  286. });
  287. // 等待开始游戏信号
  288. this.gameActive = false;
  289. }
  290. createBackground() {
  291. // 创建滚动背景
  292. this.background = this.add.tileSprite(0, 0, GAME_WIDTH, GAME_HEIGHT, 'grass');
  293. this.background.setOrigin(0, 0);
  294. }
  295. createPlayer() {
  296. // 创建玩家
  297. this.player = this.physics.add.sprite(GAME_WIDTH / 2, GAME_HEIGHT - 100, 'playerAnim');
  298. this.player.setCollideWorldBounds(true);
  299. this.player.setScale(0.8);
  300. this.player.lives = this.lives;
  301. this.player.setDepth(9);
  302. }
  303. createHUD() {
  304. // 分数显示
  305. this.scoreText = this.add.text(10, 10, `分数: ${this.score}`, {
  306. fontSize: '16px',
  307. fill: '#ffffff',
  308. backgroundColor: 'rgba(0,0,0,0.5)',
  309. padding: { x: 5, y: 2 }
  310. });
  311. this.scoreText.setDepth(10);
  312. // 生命值显示
  313. this.livesText = this.add.text(GAME_WIDTH - 80, 10, `生命: ${this.lives}`, {
  314. fontSize: '16px',
  315. fill: '#ffffff',
  316. backgroundColor: 'rgba(0,0,0,0.5)',
  317. padding: { x: 5, y: 2 }
  318. });
  319. this.livesText.setDepth(10);
  320. }
  321. initControls() {
  322. // 键盘控制
  323. this.cursors = this.input.keyboard.createCursorKeys();
  324. // 触摸控制(添加垂直移动)
  325. this.input.on('pointermove', (pointer) => {
  326. if (this.gameActive && pointer.isDown && !this.isShooting) {
  327. // 水平移动
  328. this.player.x = Phaser.Math.Clamp(pointer.x, this.player.width / 2, GAME_WIDTH - this.player.width / 2);
  329. // 垂直移动(限制范围)
  330. this.player.y = Phaser.Math.Clamp(
  331. pointer.y,
  332. 0, // 最小Y值
  333. GAME_HEIGHT// 最大Y值
  334. );
  335. }
  336. });
  337. }
  338. startGame() {
  339. // 重置核心状态
  340. this.gameActive = true;
  341. this.score = 0;
  342. this.lives = 3; // 重置生命值
  343. this.maxLives = 5;
  344. this.speed = 6; // 重置基础速度
  345. this.acceleration = 1.9; // 重置加速度
  346. this.level = 1; // 重置等级
  347. this.isSuper = false;
  348. this.isShooting = false;
  349. this.obstacles = [];
  350. this.powerUps = [];
  351. // 更新UI显示
  352. this.livesText.setText(`生命: ${this.lives}`);
  353. this.updateScore();
  354. // 清理旧定时器(双重保险)
  355. if (this.timer) this.timer.remove();
  356. if (this.obstacleEvent) this.obstacleEvent.remove();
  357. if (this.jerseyEvent) this.jerseyEvent.remove();
  358. if (this.broomEvent) this.broomEvent.remove();
  359. // 重新开始生成障碍物和计时
  360. this.startSpawning();
  361. this.timer = this.time.addEvent({
  362. delay: 200,
  363. callback: () => {
  364. this.score++;
  365. this.updateScore();
  366. if (this.score % 200 === 0) {
  367. this.levelUp();
  368. }
  369. },
  370. loop: true
  371. });
  372. }
  373. startSpawning() {
  374. // 先移除旧事件(强化清除逻辑)
  375. if (this.obstacleEvent) {
  376. this.obstacleEvent.remove();
  377. this.obstacleEvent = null; // 置空引用
  378. }
  379. if (this.jerseyEvent) {
  380. this.jerseyEvent.remove();
  381. this.jerseyEvent = null; // 置空引用
  382. }
  383. if (this.broomEvent) {
  384. this.broomEvent.remove();
  385. this.broomEvent = null; // 置空引用
  386. }
  387. // 重新创建事件(使用安全的场景上下文)
  388. this.obstacleEvent = this.time.addEvent({
  389. delay: 1000 / this.level,
  390. callback: () => { if (this.gameActive) this.spawnObstacle(); },
  391. loop: true
  392. });
  393. this.jerseyEvent = this.time.addEvent({
  394. delay: 5000,
  395. callback: () => { if (this.gameActive) this.spawnPowerUp('jersey'); },
  396. loop: true
  397. });
  398. this.broomEvent = this.time.addEvent({
  399. delay: 8000,
  400. callback: () => { if (this.gameActive) this.spawnPowerUp('broom'); },
  401. loop: true
  402. });
  403. // 新增:立即生成第一个障碍物,确保倒计时结束后立即出现
  404. this.spawnObstacle();
  405. }
  406. spawnObstacle() {
  407. const x = Phaser.Math.Between(30, GAME_WIDTH - 30);
  408. const obstacle = this.physics.add.sprite(x, -50, 'pile');
  409. obstacle.setScale(0.7);
  410. // 降低速度(从原来的this.speed * this.acceleration * 10调整为)
  411. obstacle.setVelocityY(this.speed * this.acceleration * 5); // 速度减半
  412. obstacle.setDepth(8);
  413. obstacle.type = 'pile';
  414. // 碰撞检测
  415. this.physics.add.overlap(this.player, obstacle, this.handleObstacleCollision, null, this);
  416. this.obstacles.push(obstacle);
  417. // 替换原计时器代码
  418. const event = this.time.addEvent({ // 用变量接收事件引用
  419. delay: 100,
  420. loop: true,
  421. callback: () => {
  422. if (obstacle.active) {
  423. if (obstacle.y > GAME_HEIGHT + obstacle.height) {
  424. obstacle.destroy();
  425. this.obstacles = this.obstacles.filter(o => o !== obstacle);
  426. this.score++;
  427. this.updateScore();
  428. this.time.removeEvent(event); // 移除当前事件
  429. }
  430. } else {
  431. this.time.removeEvent(event);
  432. }
  433. }
  434. });
  435. }
  436. spawnPowerUp(type) {
  437. const x = Phaser.Math.Between(30, GAME_WIDTH - 30);
  438. let powerUp;
  439. if (type === 'jersey') {
  440. powerUp = this.physics.add.sprite(x, -50, 'jersey');
  441. powerUp.type = 'jersey';
  442. } else if (type === 'broom') {
  443. powerUp = this.physics.add.sprite(x, -50, 'broom');
  444. powerUp.type = 'broom';
  445. }
  446. powerUp.setScale(0.7);
  447. // 降低道具速度(从原来的this.speed * this.acceleration * 8调整为)
  448. powerUp.setVelocityY(this.speed * this.acceleration * 4); // 速度降低
  449. powerUp.setDepth(8);
  450. // 碰撞检测
  451. this.physics.add.overlap(this.player, powerUp, this.handlePowerUpCollision, null, this);
  452. this.powerUps.push(powerUp);
  453. // 延长道具生命周期
  454. this.time.addEvent({
  455. delay: 15000,
  456. callback: () => {
  457. if (powerUp.active) {
  458. powerUp.destroy();
  459. this.powerUps = this.powerUps.filter(p => p !== powerUp);
  460. }
  461. }
  462. });
  463. }
  464. handleObstacleCollision(player, obstacle) {
  465. if (this.isSuper) {
  466. // 超级状态下直接摧毁障碍物
  467. obstacle.destroy();
  468. this.obstacles = this.obstacles.filter(o => o !== obstacle);
  469. return;
  470. }
  471. // 扣除生命值
  472. player.lives--;
  473. this.lives = player.lives;
  474. this.livesText.setText(`生命: ${this.lives}`);
  475. // 显示受伤效果
  476. player.setTexture('playerCollision');
  477. this.time.addEvent({
  478. delay: 618,
  479. callback: () => {
  480. player.setTexture('playerAnim');
  481. }
  482. });
  483. // 销毁障碍物
  484. obstacle.destroy();
  485. this.obstacles = this.obstacles.filter(o => o !== obstacle);
  486. // 检查游戏是否结束
  487. if (player.lives <= 0) {
  488. this.gameOver();
  489. }
  490. }
  491. handlePowerUpCollision(player, powerUp) {
  492. if (powerUp.type === 'jersey') {
  493. // 增加生命值
  494. if (player.lives < this.maxLives) {
  495. player.lives++;
  496. this.lives = player.lives;
  497. this.livesText.setText(`生命: ${this.lives}`);
  498. }
  499. } else if (powerUp.type === 'broom') {
  500. // 激活超级状态
  501. this.activateSuperMode();
  502. }
  503. // 销毁道具
  504. powerUp.destroy();
  505. this.powerUps = this.powerUps.filter(p => p !== powerUp);
  506. }
  507. activateSuperMode() {
  508. this.isSuper = true;
  509. this.player.setTexture('playerSuper');
  510. this.acceleration = 10;
  511. // 3秒后结束超级状态
  512. this.time.addEvent({
  513. delay: 3000,
  514. callback: () => {
  515. this.isSuper = false;
  516. this.player.setTexture('playerAnim');
  517. this.acceleration = 1.9 + (this.level - 1) * 0.5;
  518. }
  519. });
  520. }
  521. levelUp() {
  522. this.level++;
  523. this.acceleration += 0.5;
  524. // 显示升级提示
  525. const levelUpText = this.add.text(GAME_WIDTH / 2, GAME_HEIGHT / 2, `Level ${this.level}!`, {
  526. fontSize: '32px',
  527. fill: '#ffff00',
  528. stroke: '#000000',
  529. strokeThickness: 2
  530. }).setOrigin(0.5);
  531. levelUpText.setDepth(10);
  532. this.time.addEvent({
  533. delay: 1000,
  534. callback: () => {
  535. levelUpText.destroy();
  536. }
  537. });
  538. }
  539. updateScore() {
  540. try {
  541. // 关键:检查DOM元素和场景是否存在
  542. if (!this.scoreText || !this.scene) return;
  543. this.scoreText.setText(`分数: ${this.score}`);
  544. // 触发全局分数更新事件(添加安全检查)
  545. if (this.game && this.game.events) {
  546. this.game.events.emit('scoreChanged', this.score);
  547. }
  548. // 每200分触发一次射门环节
  549. if (this.score % 200 === 0 && this.score > 0 && !this.isShooting) {
  550. this.enterShootingMode();
  551. }
  552. } catch (error) {
  553. console.error('updateScore 错误:', error);
  554. }
  555. }
  556. enterShootingMode() {
  557. this.gameActive = false;
  558. this.isShooting = true;
  559. // 关键修复:重置键盘状态,清除所有按键的按下状态
  560. this.input.keyboard.resetKeys();
  561. // 额外保险:手动清除方向键状态
  562. this.cursors.left.isDown = false;
  563. this.cursors.right.isDown = false;
  564. this.cursors.up.isDown = false;
  565. this.cursors.down.isDown = false;
  566. // 停止所有生成事件
  567. if (this.obstacleEvent) this.obstacleEvent.remove();
  568. if (this.jerseyEvent) this.jerseyEvent.remove();
  569. if (this.broomEvent) this.broomEvent.remove();
  570. // 清除现有障碍物和道具
  571. this.obstacles.forEach(obs => obs.destroy());
  572. this.obstacles = [];
  573. this.powerUps.forEach(p => p.destroy());
  574. this.powerUps = [];
  575. // 创建射门场景背景
  576. this.background.setTexture('goalBackground');
  577. // 创建球门
  578. this.goal = this.physics.add.sprite(GAME_WIDTH / 2, 100, 'goal');
  579. this.goal.setScale(0.8);
  580. this.goal.setImmovable(true);
  581. this.goal.setDepth(5);
  582. // 创建守门员
  583. this.goalkeeper = this.physics.add.sprite(GAME_WIDTH / 2, 150, 'goalkeeperAnim');
  584. this.goalkeeper.setScale(0.9);
  585. this.goalkeeper.setImmovable(true);
  586. this.goalkeeper.anims.play('goalkeeperAnim', true);
  587. this.goalkeeper.setDepth(7);
  588. // 让守门员左右移动
  589. this.tweens.add({
  590. targets: this.goalkeeper,
  591. x: [GAME_WIDTH / 2 - 50, GAME_WIDTH / 2 + 50],
  592. duration: 2000,
  593. ease: 'Sine.inOut',
  594. repeat: -1,
  595. yoyo: true
  596. });
  597. // 调整球员位置(准备射门)
  598. this.player.setPosition(GAME_WIDTH / 2, GAME_HEIGHT - 100);
  599. this.player.setTexture('playerShoot');
  600. // 关键修复:停止玩家所有移动
  601. this.player.setVelocity(0);
  602. // 创建足球(修复:让足球跟随玩家位置)
  603. this.ball = this.physics.add.sprite(this.player.x, this.player.y - 50, 'ballAnim');
  604. this.ball.setScale(0.8);
  605. this.ball.anims.play('ballAnim', true);
  606. this.ball.setDepth(8);
  607. // 显示射门提示
  608. this.shootHint = this.add.text(GAME_WIDTH / 2, GAME_HEIGHT - 50, '点击或按空格键射门', {
  609. fontSize: '16px',
  610. fill: '#ffffff',
  611. backgroundColor: 'rgba(0,0,0,0.5)',
  612. padding: { x: 5, y: 2 }
  613. }).setOrigin(0.5);
  614. this.shootHint.setDepth(10);
  615. // 射门控制
  616. this.input.keyboard.on('keydown-SPACE', this.shootBall, this);
  617. this.input.on('pointerdown', this.shootBall, this);
  618. }
  619. shootBall() {
  620. if (!this.isShooting) return;
  621. // 禁用输入
  622. this.input.keyboard.off('keydown-SPACE', this.shootBall, this);
  623. this.input.off('pointerdown', this.shootBall, this);
  624. // 移除射门模式下的左右移动监听(如果添加了)
  625. this.input.keyboard.off('keydown-LEFT');
  626. this.input.keyboard.off('keydown-RIGHT');
  627. if (this.shootHint) {
  628. this.shootHint.destroy();
  629. this.shootHint = null;
  630. }
  631. // 计算龙门网内的目标位置
  632. const goalNetY = this.goal.y + 30;
  633. const goalNetX = this.player.x;
  634. // 创建射门动画
  635. const ballTween = this.tweens.add({
  636. targets: this.ball,
  637. x: goalNetX,
  638. y: goalNetY,
  639. duration: 1000,
  640. ease: 'Power1',
  641. onComplete: () => {
  642. if (!this.ball || !this.ball.active) return;
  643. // 判断是否成功
  644. const isSuccess = Phaser.Math.Between(0, 1) === 1;
  645. if (isSuccess) {
  646. this.score += 3;
  647. this.updateScore();
  648. this.ball.setDepth(6.5); // 成功:足球在守门员后方(网内)
  649. this.successShoot();
  650. } else {
  651. this.ball.setDepth(7.5); // 失败:足球在守门员前方
  652. this.failShoot();
  653. }
  654. ballTween.remove();
  655. }
  656. });
  657. }
  658. // 射门成功处理
  659. successShoot() {
  660. this.isShooting = false;
  661. // 显示成功提示
  662. const successText = this.add.text(GAME_WIDTH / 2, GAME_HEIGHT / 2, '射门成功!+3分', {
  663. fontSize: '24px',
  664. fill: '#0f0',
  665. stroke: '#000000',
  666. strokeThickness: 2
  667. }).setOrigin(0.5);
  668. successText.setDepth(10);
  669. // 2秒后返回奔跑场景
  670. this.time.addEvent({
  671. delay: 2000,
  672. callback: () => {
  673. if (successText && successText.active) {
  674. successText.destroy();
  675. }
  676. this.ball.destroy();
  677. this.resetToRunningScene();
  678. },
  679. callbackScope: this
  680. });
  681. }
  682. // 射门失败处理
  683. failShoot() {
  684. this.isShooting = false;
  685. // 关键修复1:解除足球的物理控制
  686. this.ball.setVelocity(0);
  687. this.ball.setImmovable(true);
  688. this.physics.world.disable(this.ball); // 完全禁用物理引擎对足球的控制
  689. // 关键修复2:调整足球位置到守门员前方固定位置,不跟随移动
  690. this.ball.setPosition(
  691. this.goalkeeper.x,
  692. this.goalkeeper.y + 20 // 固定在守门员前方20px处
  693. );
  694. // 显示失败提示
  695. const failText = this.add.text(GAME_WIDTH / 2, GAME_HEIGHT / 2, '射门失败!', {
  696. fontSize: '24px',
  697. fill: '#f00',
  698. stroke: '#000000',
  699. strokeThickness: 2
  700. }).setOrigin(0.5);
  701. failText.setDepth(10);
  702. // 2秒后返回奔跑场景
  703. this.time.addEvent({
  704. delay: 2000,
  705. callback: () => {
  706. if (failText && failText.active) {
  707. failText.destroy();
  708. }
  709. this.ball.destroy(); // 确保足球被销毁
  710. this.resetToRunningScene();
  711. },
  712. callbackScope: this
  713. });
  714. }
  715. // 重置为奔跑场景
  716. resetToRunningScene() {
  717. this.clearScene();
  718. // 重置玩家状态
  719. if (this.player) {
  720. this.player.setTexture('playerAnim');
  721. this.player.setPosition(GAME_WIDTH / 2, GAME_HEIGHT - 100);
  722. this.player.setVelocity(0);
  723. }
  724. // 恢复背景和游戏状态
  725. if (this.background) this.background.setTexture('grass');
  726. this.gameActive = true;
  727. this.isShooting = false;
  728. // 关键:确保所有射门场景元素被彻底清理
  729. if (this.ball) {
  730. this.ball.destroy();
  731. this.ball = null; // 显式置空引用
  732. }
  733. // 重新开始生成障碍物
  734. this.startSpawning();
  735. }
  736. gameOver() {
  737. this.gameActive = false;
  738. // 清理所有定时事件
  739. if (this.timer) this.timer.remove();
  740. if (this.obstacleEvent) this.obstacleEvent.remove();
  741. if (this.jerseyEvent) this.jerseyEvent.remove();
  742. if (this.broomEvent) this.broomEvent.remove();
  743. // 彻底清理计时相关资源
  744. if (this.timer) {
  745. this.timer.destroy(); // 销毁计时器(比remove()更彻底)
  746. this.timer = null; // 置空引用,防止残留
  747. }
  748. // 显示游戏结束画面
  749. this.add.image(GAME_WIDTH / 2, GAME_HEIGHT / 2, 'gameOver').setOrigin(0.5);
  750. // 触发游戏结束事件
  751. this.time.addEvent({
  752. delay: 2000,
  753. callback: () => {
  754. this.game.events.emit('gameOver');
  755. }
  756. });
  757. }
  758. update() {
  759. if (!this.gameActive) return;
  760. // 关键修复:射门模式下直接返回,不执行任何移动逻辑
  761. if (this.isShooting) return;
  762. // 玩家移动逻辑(原有代码保持不变)
  763. if (this.player && this.player.active) {
  764. if (this.cursors.left.isDown) {
  765. this.player.setVelocityX(-200);
  766. if (this.player.anims.currentAnim?.key !== 'playerLeft' && !this.player.anims.paused) {
  767. this.player.anims.play('playerLeft', true);
  768. }
  769. } else if (this.cursors.right.isDown) {
  770. this.player.setVelocityX(200);
  771. if (this.player.anims.currentAnim?.key !== 'playerRight' && !this.player.anims.paused) {
  772. this.player.anims.play('playerRight', true);
  773. }
  774. } else if (this.cursors.up.isDown) {
  775. this.player.setVelocityY(-200);
  776. if (this.player.anims.currentAnim?.key === 'playerLeft' && !this.player.anims.paused) {
  777. this.player.anims.play('playerLeft', true);
  778. } else if (this.player.anims.currentAnim?.key === 'playerRight' && !this.player.anims.paused) {
  779. this.player.anims.play('playerRight', true);
  780. } else {
  781. this.player.anims.play('playerLeft', true);
  782. }
  783. } else if (this.cursors.down.isDown) {
  784. this.player.setVelocityY(200);
  785. if (this.player.anims.currentAnim?.key === 'playerLeft' && !this.player.anims.paused) {
  786. this.player.anims.play('playerLeft', true);
  787. } else if (this.player.anims.currentAnim?.key === 'playerRight' && !this.player.anims.paused) {
  788. this.player.anims.play('playerRight', true);
  789. } else {
  790. this.player.anims.play('playerLeft', true);
  791. }
  792. } else {
  793. this.player.setVelocityX(0);
  794. this.player.setVelocityY(0);
  795. this.player.anims.stop();
  796. }
  797. }
  798. }
  799. // 添加 clearScene 方法
  800. clearScene() {
  801. // 清理射门场景的元素
  802. if (this.goal) this.goal.destroy();
  803. if (this.goalkeeper) this.goalkeeper.destroy();
  804. if (this.ball) {
  805. this.ball.destroy();
  806. this.ball = null; // 显式置空
  807. }
  808. if (this.shootHint) this.shootHint.destroy();
  809. // 清理所有障碍物和道具
  810. this.obstacles.forEach(obs => obs.destroy());
  811. this.powerUps.forEach(p => p.destroy());
  812. this.obstacles = [];
  813. this.powerUps = [];
  814. // 停止所有动画
  815. this.tweens.killAll();
  816. }
  817. // 新增:场景销毁时清理资源
  818. destroy() {
  819. // 清理所有定时器
  820. if (this.timer) this.timer.remove();
  821. if (this.obstacleEvent) this.obstacleEvent.remove();
  822. if (this.jerseyEvent) this.jerseyEvent.remove();
  823. if (this.broomEvent) this.broomEvent.remove();
  824. // 移除输入监听
  825. this.input.keyboard.off('keydown-SPACE', this.shootBall, this);
  826. this.input.off('pointerdown', this.shootBall, this);
  827. // 清理所有游戏对象
  828. this.obstacles.forEach(obs => obs.destroy());
  829. this.powerUps.forEach(p => p.destroy());
  830. if (this.goal) this.goal.destroy();
  831. if (this.goalkeeper) this.goalkeeper.destroy();
  832. if (this.ball) this.ball.destroy();
  833. // 调用父类销毁方法
  834. super.destroy();
  835. }
  836. }
  837. // 游戏控制函数
  838. const startGame = () => {
  839. currentScene.value = 'countdown';
  840. // 倒计时
  841. currentScene.value = 'countdown';
  842. let count = 3;
  843. const countdownInterval = setInterval(() => {
  844. count--;
  845. countdownNum.value = count;
  846. if (count <= 0) {
  847. clearInterval(countdownInterval);
  848. currentScene.value = 'game';
  849. // 通知游戏开始
  850. if (game.value) {
  851. const gameScene = game.value.scene.getScene('GameScene');
  852. if (gameScene) {
  853. gameScene.startGame();
  854. }
  855. }
  856. }
  857. }, 1000);
  858. };
  859. const continueGame = () => {
  860. startGame();
  861. };
  862. const restartGame = () => {
  863. // 1. 彻底销毁旧游戏实例(含场景和资源)
  864. if (game.value) {
  865. // 先停止游戏逻辑
  866. const gameScene = game.value.scene.getScene('GameScene');
  867. if (gameScene) {
  868. gameScene.gameActive = false;
  869. }
  870. // 销毁游戏实例(强制清理DOM)
  871. game.value.destroy(true);
  872. game.value = null; // 置空引用,避免残留
  873. }
  874. // 2. 重置全局状态
  875. score.value = 0;
  876. currentScene.value = 'start';
  877. countdownNum.value = 3;
  878. // 3. 延迟初始化新游戏(确保旧实例完全销毁)
  879. setTimeout(() => {
  880. initGame();
  881. }, 100); // 短暂延迟确保DOM清理完成
  882. };
  883. const getTitle = (score) => {
  884. if (score < 50) return '足球新手';
  885. if (score < 100) return '业余球员';
  886. if (score < 200) return '专业选手';
  887. if (score < 300) return '足球明星';
  888. return '足球传奇';
  889. };
  890. // 组件挂载时初始化游戏
  891. onMounted(() => {
  892. initGame();
  893. });
  894. // 监听场景变化
  895. watch(currentScene, (newVal) => {
  896. if (newVal === 'game' && game.value) {
  897. const gameScene = game.value.scene.getScene('GameScene');
  898. if (gameScene) {
  899. gameScene.scene.resume();
  900. }
  901. }
  902. });
  903. // 组件卸载时清理事件监听
  904. onUnmounted(() => {
  905. if (game.value && game.value.events && scoreListener) {
  906. game.value.events.off('scoreChanged', scoreListener);
  907. }
  908. });
  909. </script>
  910. <style scoped>
  911. .game-container {
  912. position: relative;
  913. width: 100%;
  914. height: 100vh;
  915. margin: 0 auto;
  916. overflow: hidden;
  917. }
  918. .game-canvas {
  919. width: 100%;
  920. height: 100%;
  921. background-color: #000;
  922. }
  923. .gamestart {
  924. position: absolute;
  925. top: 0;
  926. left: 0;
  927. width: 100%;
  928. height: 100%;
  929. z-index: 100;
  930. }
  931. .start_bg {
  932. width: 100%;
  933. height: 100%;
  934. object-fit: cover;
  935. }
  936. .btn {
  937. width: 80%;
  938. height: 40px;
  939. border-radius: 5px;
  940. text-align: center;
  941. line-height: 40px;
  942. color: #333;
  943. box-shadow: 0 5px 0px #07942c;
  944. box-shadow: 0 5px 0px rgba(0, 0, 0, 0.17);
  945. position: absolute;
  946. left: 50%;
  947. margin: 0 0 0 -40%;
  948. font-size: 18px;
  949. }
  950. .rule {
  951. background: #fff;
  952. bottom: 100px;
  953. }
  954. .start {
  955. background: #ffff00;
  956. bottom: 40px;
  957. }
  958. .ruleshadow {
  959. position: absolute;
  960. top: 0;
  961. left: 0;
  962. width: 100%;
  963. height: 100%;
  964. background-color: rgba(0, 0, 0, 0.7);
  965. z-index: 102;
  966. display: flex;
  967. justify-content: center;
  968. align-items: center;
  969. font-size: 18px;
  970. }
  971. .rulebox {
  972. width: 280px;
  973. background-color: white;
  974. border-radius: 10px;
  975. padding: 20px;
  976. position: relative;
  977. opacity: 0;
  978. animation: fadeIn 0.5s forwards;
  979. }
  980. @keyframes fadeIn {
  981. to {
  982. opacity: 1;
  983. }
  984. }
  985. .x_rulebox {
  986. position: absolute;
  987. top: 10px;
  988. right: 10px;
  989. width: 20px;
  990. height: 20px;
  991. 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");
  992. cursor: pointer;
  993. }
  994. .daoju {
  995. margin-top: 15px;
  996. }
  997. .daoju_item {
  998. display: flex;
  999. align-items: center;
  1000. margin-bottom: 10px;
  1001. }
  1002. .daoju_1,
  1003. .daoju_2,
  1004. .daoju_3 {
  1005. display: inline-block;
  1006. width: 60px;
  1007. height: 60px;
  1008. margin-right: 10px;
  1009. background-size: contain;
  1010. background-repeat: no-repeat;
  1011. flex-shrink: 0;
  1012. }
  1013. .daoju_1 {
  1014. background-image: url("/static/images/football/Jersey.png");
  1015. }
  1016. .daoju_2 {
  1017. background-image: url("/static/images/football/Broom.png");
  1018. }
  1019. .daoju_3 {
  1020. background-image: url("/static/images/football/Pile.png");
  1021. }
  1022. .zhiyin {
  1023. position: absolute;
  1024. top: 0;
  1025. left: 0;
  1026. width: 100%;
  1027. height: 100%;
  1028. background-color: rgba(0, 0, 0, 0.7);
  1029. z-index: 100;
  1030. display: flex;
  1031. justify-content: center;
  1032. align-items: center;
  1033. }
  1034. .daojishi {
  1035. position: absolute;
  1036. width: 100%;
  1037. top: 0;
  1038. bottom: 0;
  1039. display: none;
  1040. z-index: 11;
  1041. background: url(daojishu_bg.png) repeat left top;
  1042. background-size: 100%;
  1043. }
  1044. .daojishiline {
  1045. width: 100%;
  1046. height: 95px;
  1047. position: absolute;
  1048. top: 109px;
  1049. left: 0;
  1050. background: #ffe400;
  1051. z-index: 1;
  1052. }
  1053. .daojishibox {
  1054. width: 180px;
  1055. height: 180px;
  1056. position: absolute;
  1057. left: 50%;
  1058. top: 71px;
  1059. margin: 0 0 0 -90px;
  1060. background: url(daojishibox.png) no-repeat left top;
  1061. background-size: 100%;
  1062. z-index: 2;
  1063. }
  1064. .daojishipangzi {
  1065. width: 111px;
  1066. height: 102px;
  1067. position: absolute;
  1068. left: 50%;
  1069. bottom: 50px;
  1070. margin: 0 0 0 -55px;
  1071. background: url(daojishipangzi.png) no-repeat left top;
  1072. background-size: 100%;
  1073. z-index: -1;
  1074. }
  1075. .daojishinum {
  1076. width: 100%;
  1077. text-align: center;
  1078. font-size: 80px;
  1079. color: #FFFFFF;
  1080. padding: 70px 0 0 0;
  1081. line-height: normal;
  1082. }
  1083. @keyframes countDown {
  1084. 0% {
  1085. transform: scale(3);
  1086. opacity: 0;
  1087. }
  1088. 50% {
  1089. transform: scale(1.2);
  1090. opacity: 1;
  1091. }
  1092. 100% {
  1093. transform: scale(1);
  1094. opacity: 1;
  1095. }
  1096. }
  1097. .gameend {
  1098. position: absolute;
  1099. top: 0;
  1100. left: 0;
  1101. width: 100%;
  1102. height: 100%;
  1103. background-color: rgba(0, 0, 0, 0.7);
  1104. z-index: 100;
  1105. display: flex;
  1106. flex-direction: column;
  1107. justify-content: center;
  1108. align-items: center;
  1109. color: white;
  1110. }
  1111. .game_fenshu {
  1112. font-size: 36px;
  1113. margin-bottom: 20px;
  1114. }
  1115. .chenghao {
  1116. font-size: 24px;
  1117. margin-bottom: 30px;
  1118. text-align: center;
  1119. }
  1120. .game_ogain,
  1121. .game_chakan,
  1122. .game_share {
  1123. width: 150px;
  1124. height: 40px;
  1125. line-height: 40px;
  1126. text-align: center;
  1127. background-color: #f00;
  1128. border-radius: 20px;
  1129. margin-bottom: 10px;
  1130. cursor: pointer;
  1131. font-size: 18px;
  1132. }
  1133. .game_chakan {
  1134. background-color: #666;
  1135. }
  1136. .game_share {
  1137. background-color: #008000;
  1138. }
  1139. </style>