basketball.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965
  1. <template>
  2. <div class="game-container">
  3. <canvas :id="'canvas' + type" @mousedown="handleMouseDown" @mouseup="handleMouseUp" @touchstart="handleTouchStart"
  4. @touchend="handleTouchEnd"></canvas>
  5. <canvas ref="canvasRef" :width="clientObj.width" :height="clientObj.height"
  6. style="position:absolute;left: 0; top: 0;"></canvas>
  7. </div>
  8. </template>
  9. <script setup name="Basketball" lang="ts">
  10. import { onMounted, ref, reactive, onBeforeUnmount } from 'vue';
  11. const { proxy } = getCurrentInstance() as any;
  12. const router = useRouter();
  13. const emit = defineEmits(['confirmExit']);
  14. const canvasRef = ref(null);
  15. //父值
  16. const props = defineProps({
  17. type: {
  18. type: String,
  19. default: ""
  20. },
  21. currentGameArea: {
  22. type: String,
  23. default: ""
  24. },
  25. areaStateList: {
  26. type: Array,
  27. default: () => []
  28. },
  29. });
  30. const data = reactive<any>({
  31. clientObj: {},//浏览器对象
  32. boxes: [],//四个点坐标
  33. proportion: null,//人框和屏幕比例
  34. myThrow: 0,//0放下1举投
  35. direction: null,//跑动
  36. });
  37. const { clientObj, boxes, proportion, myThrow, direction } = toRefs(data);
  38. /**
  39. * 监听投篮
  40. */
  41. watch(
  42. () => myThrow.value,
  43. (newData, oldData) => {
  44. if (oldData == undefined) {
  45. return false;
  46. }
  47. if (gameState.state !== 'play') return;
  48. console.log("ppp", oldData, newData)
  49. if (newData == 1) {
  50. console.log("投篮")
  51. handleMouseDown();
  52. setTimeout(() => {
  53. handleMouseUp();
  54. }, 100)
  55. } else {
  56. }
  57. },
  58. { immediate: true }
  59. );
  60. /**
  61. * 监听跑动
  62. */
  63. watch(
  64. () => direction.value,
  65. (newData, oldData) => {
  66. nextTick(() => {
  67. if (newData > oldData) {
  68. gameState.keyRight = true;
  69. gameState.keyLeft = false;
  70. } else {
  71. gameState.keyLeft = true;
  72. gameState.keyRight = false;
  73. }
  74. });
  75. },
  76. { immediate: true }
  77. );
  78. /**
  79. * 初始化
  80. */
  81. const getInit = (e: any) => {
  82. let arr = e.data.result.keypoints;
  83. let result = [];
  84. for (let i = 0; i < arr.length; i += 3) {
  85. result.push(arr.slice(i, i + 2));
  86. }
  87. //console.log("result", result)
  88. // if (boxes.value.length == 0) {
  89. // // speckText("识别成功");
  90. // // proxy?.$modal.msgWarning(`识别成功`);
  91. // let arr = e.data.result.boxes;
  92. // 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] }]
  93. // proportion.value = (clientObj.value.height / (boxes.value[0].y - boxes.value[1].y)).toFixed(2);
  94. // }
  95. getCanvas(result);
  96. };
  97. const getCanvas = (result: any) => {
  98. //过肩或者过鼻子都算投篮
  99. let leftA = result[6][1];//右肩Y
  100. let rightA = result[5][1];//左肩Y
  101. let leftB = result[10][1];//右手Y
  102. let rightB = result[9][1];//左手Y
  103. let bizi = result[0][1];//鼻子Y
  104. if (leftB < leftA || rightB < rightA || leftB < bizi || rightB < bizi) {
  105. myThrow.value = 1;
  106. } else {
  107. myThrow.value = 0;
  108. }
  109. const canvas: any = canvasRef.value;
  110. const ctx = canvas.getContext('2d');
  111. // 清空整个画布
  112. ctx.clearRect(0, 0, canvas.width, canvas.height);
  113. // 保存当前状态
  114. ctx.save();
  115. function calculateOffset(a: any, b: any) {
  116. return {
  117. x: b.x - a.x,
  118. y: b.y - a.y
  119. };
  120. }
  121. const pointA = { x: clientObj.value.width / 2, y: clientObj.value.height / 2 };
  122. const pointB = { x: (boxes.value[2].x + boxes.value[0].x) / 2, y: (boxes.value[3].y + boxes.value[1].y) / 2 };
  123. const offset = calculateOffset(pointA, pointB);
  124. ctx.translate(-offset.x, -offset.y);
  125. // console.log("Canvas分辨率", clientObj.value);
  126. // console.log("人体图片四点坐标", boxes.value)
  127. // console.log("Canvas中心", pointA);
  128. // console.log("人体中心", pointB);
  129. // console.log("offset", offset)
  130. // console.log("proportion.value",proportion.value)
  131. const originalPoints = result;
  132. // 计算缩放后坐标
  133. const postData = originalPoints.map((point: any) => {
  134. const newX = (point[0] - pointB.x) * proportion.value + pointB.x;
  135. const newY = (point[1] - pointB.y) * proportion.value + pointB.y;
  136. return [newX, newY];
  137. });
  138. // console.log("原始坐标:", originalPoints);
  139. // console.log("缩放后坐标:", postData);
  140. direction.value = postData[0][0] - offset.x - (94 / 2);//鼻子X
  141. //绘制头部
  142. const point1 = { x: postData[4][0], y: postData[4][1] };
  143. const point2 = { x: postData[3][0], y: postData[3][1] };
  144. // 计算椭圆参数
  145. const centerX = (point1.x + point2.x) / 2; // 椭圆中心X
  146. const centerY = (point1.y + point2.y) / 2; // 椭圆中心Y
  147. const distance = Math.sqrt(
  148. Math.pow(point2.x - point1.x, 2) +
  149. Math.pow(point2.y - point1.y, 2)
  150. ); // 两个焦点之间的距离
  151. const radiusX = distance * 0.5; // 水平半径(可调整)
  152. const radiusY = distance * 0.6; // 垂直半径(可调整)
  153. // 1. 绘制填充椭圆
  154. ctx.beginPath();
  155. ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2);
  156. ctx.fillStyle = 'red'; // 填充颜色
  157. ctx.fill(); // 填充
  158. // 2. 绘制边框
  159. ctx.strokeStyle = 'red';
  160. ctx.lineWidth = 5;
  161. ctx.stroke();
  162. // 绘制每个点
  163. postData.forEach((point: any, index: number) => {
  164. //眼睛鼻子不显示
  165. if (![0, 1, 2].includes(index)) {
  166. const [x, y] = point;
  167. ctx.beginPath();
  168. ctx.arc(x, y, 5, 0, Math.PI * 2); // 绘制半径为5的圆点
  169. ctx.fillStyle = 'red';
  170. ctx.fill();
  171. ctx.lineWidth = 1;
  172. ctx.stroke();
  173. }
  174. });
  175. // 根据点关系连线
  176. 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]]
  177. arr.forEach((point: any) => {
  178. let index1 = point[0];
  179. let index2 = point[1];
  180. //连线
  181. const dian1 = { x: postData[index1][0], y: postData[index1][1] };
  182. const dian2 = { x: postData[index2][0], y: postData[index2][1] };
  183. // 绘制连线
  184. ctx.beginPath();
  185. ctx.moveTo(dian1.x, dian1.y); // 起点
  186. ctx.lineTo(dian2.x, dian2.y); // 终点
  187. ctx.strokeStyle = 'red'; // 线条颜色
  188. ctx.lineWidth = 3; // 线条宽度
  189. ctx.stroke(); // 描边
  190. });
  191. ctx.restore(); // 恢复状态
  192. };
  193. // 游戏主类的响应式状态
  194. const gameState = reactive({
  195. balls: [],
  196. hoops: [],
  197. texts: [],
  198. res: {},
  199. score: 0,
  200. started: false,
  201. gameOver: false,
  202. ballX: (640 - 93) / 2, // 篮球初始X坐标(居中)
  203. ballY: 680, // 篮球初始Y坐标(底部)
  204. ballVel: 300,
  205. ballAngleVel: 100,
  206. ballAngle: 0,
  207. ballsShot: 1,
  208. ballCharge: 0,
  209. time: 60,
  210. toNextSecond: 1,
  211. sound: false,
  212. state: 'menu',
  213. click: false,
  214. canvas: null,
  215. ctx: null,
  216. animationFrameId: null,
  217. then: 0,
  218. // 新增:键盘控制相关状态
  219. keyLeft: false,
  220. keyRight: false,
  221. spaceShoot: false, // 空格键投篮状态
  222. exitTimer: null, // 用于存储退出定时器ID
  223. bounceHeight: 20, // 拍球高度
  224. bounceSpeed: 2, // 拍球速度
  225. bouncePhase: 0, // 拍球相位(0-2π)
  226. });
  227. // 篮筐类 - 修改后
  228. class Hoop {
  229. constructor(x, y) {
  230. this.x = x;
  231. this.y = y;
  232. this.originalX = x;
  233. this.speed = 80; // 降低速度,适应更大尺寸
  234. this.direction = 1;
  235. // 滑动范围适配更大篮筐
  236. this.range = (clientObj.value.width - 300) / 2;
  237. // 碰撞点范围扩大(与视觉尺寸匹配)
  238. this.points = [
  239. { x: x + 20, y: y + 30 },
  240. { x: x + 280, y: y + 30 }
  241. ];
  242. // 新增:缩放比例(1.5倍放大)
  243. this.scale = 1.5;
  244. }
  245. update(delta) {
  246. this.x += this.speed * this.direction * delta;
  247. // 边界检测调整
  248. if (this.x > this.originalX + this.range) {
  249. this.x = this.originalX + this.range;
  250. this.direction = -1;
  251. } else if (this.x < this.originalX - this.range) {
  252. this.x = this.originalX - this.range;
  253. this.direction = 1;
  254. }
  255. // 更新碰撞点(与放大后的尺寸匹配)
  256. this.points = [
  257. { x: this.x + 20, y: this.y + 30 },
  258. { x: this.x + 280, y: this.y + 30 }
  259. ];
  260. }
  261. drawBack(ctx, game) {
  262. // 关键修改:通过scale参数放大绘制
  263. ctx.save();
  264. ctx.translate(this.x, this.y);
  265. ctx.scale(this.scale, this.scale); // 放大1.5倍
  266. drawImage(
  267. ctx,
  268. game.res['/static/images/basketball/hoop.png'],
  269. 0, 0, // 绘制起点(相对于缩放中心)
  270. 0, 0, 148, 22, // 原图裁剪范围
  271. 0, 0, 0 // 旋转参数
  272. );
  273. ctx.restore();
  274. }
  275. drawFront(ctx, game) {
  276. // 同样放大前景
  277. ctx.save();
  278. ctx.translate(this.x, this.y + 22 * this.scale); // 适配缩放后的Y位置
  279. ctx.scale(this.scale, this.scale);
  280. drawImage(
  281. ctx,
  282. game.res['/static/images/basketball/hoop.png'],
  283. 0, 0,
  284. 0, 22, 148, 178 - 22, // 原图裁剪范围
  285. 0, 0, 0
  286. );
  287. ctx.restore();
  288. }
  289. }
  290. // 篮球类
  291. class Ball {
  292. constructor(x, y) {
  293. this.x = x;
  294. this.y = y;
  295. this.vx = 0;
  296. this.vy = 0;
  297. this.speed = 100;
  298. this.canBounce = true;
  299. this.angle = 270;
  300. this.gravity = 0;
  301. this.falling = false;
  302. this.bounces = 0;
  303. this.scored = false;
  304. this.drawAngle = 0;
  305. this.angleVel = 150;
  306. this.solid = false;
  307. this.z = 1;
  308. }
  309. setAngle(angle) {
  310. this.angle = angle;
  311. this.vx = this.speed * Math.cos(this.angle * Math.PI / 180);
  312. this.vy = this.speed * Math.sin(this.angle * Math.PI / 180);
  313. this.gravity = 0;
  314. }
  315. shoot(power) {
  316. this.speed = power;
  317. this.setAngle(270);
  318. }
  319. update(delta) {
  320. this.y += this.gravity * delta;
  321. this.gravity += 10000 * delta;
  322. this.x += this.vx * delta;
  323. this.y += this.vy * delta;
  324. if (this.vx > 500) this.vx = 500;
  325. if (this.vy > 500) this.vy = 500;
  326. if (this.y < 300) {
  327. this.solid = true;
  328. }
  329. if (this.gravity > this.speed) {
  330. this.falling = true;
  331. }
  332. if (this.x + 47 > clientObj.value.width) {
  333. this.vx = this.vx * -1;
  334. this.x = clientObj.value.width - 47;
  335. }
  336. if (this.x - 47 < 0) {
  337. this.vx = this.vx * -1;
  338. this.x = 47;
  339. }
  340. this.drawAngle += this.angleVel * delta;
  341. }
  342. draw(ctx, game) {
  343. drawImage(
  344. ctx,
  345. game.res['/static/images/basketball/ball.png'],
  346. Math.floor(this.x - (93 / 2)),
  347. Math.floor(this.y - (93 / 2)),
  348. 0, 0, 93, 93, 93 / 2, 93 / 2,
  349. this.drawAngle
  350. );
  351. }
  352. }
  353. // 弹出文字类
  354. class PopText {
  355. constructor(string, x, y) {
  356. this.string = string;
  357. this.x = x;
  358. this.y = y;
  359. this.vy = -500;
  360. this.opacity = 1;
  361. }
  362. update(delta) {
  363. this.y += this.vy * delta;
  364. this.vy += 1000 * delta;
  365. if (this.vy > 0 && this.opacity > 0) {
  366. this.opacity -= 2 * delta;
  367. }
  368. if (this.opacity <= 0) {
  369. this.opacity = 0;
  370. }
  371. }
  372. draw(ctx, game) {
  373. ctx.globalAlpha = this.opacity;
  374. game.drawText(ctx, this.string, this.x + 15, this.y);
  375. ctx.globalAlpha = 1;
  376. }
  377. }
  378. // 工具函数:绘制旋转图像
  379. function drawImage(ctx, image, x, y, sx, sy, w, h, rx, ry, a) {
  380. ctx.save();
  381. ctx.translate(x + rx, y + ry);
  382. ctx.rotate(a * Math.PI / 180);
  383. ctx.drawImage(image, sx, sy, w, h, -rx, -ry, w, h);
  384. ctx.restore();
  385. }
  386. // 事件处理函数
  387. const handleMouseDown = () => {
  388. gameState.click = true;
  389. };
  390. const handleMouseUp = () => {
  391. gameState.click = false;
  392. };
  393. const handleTouchStart = () => {
  394. gameState.click = true;
  395. };
  396. const handleTouchEnd = () => {
  397. gameState.click = false;
  398. };
  399. // 键盘事件处理函数
  400. const handleKeyDown = (event: any) => {
  401. if (gameState.state !== 'play') return;
  402. if (event.key === 'ArrowLeft') {
  403. gameState.keyLeft = true;
  404. } else if (event.key === 'ArrowRight') {
  405. gameState.keyRight = true;
  406. } else if (event.key === ' ') { // 空格键触发投篮
  407. event.preventDefault(); // 防止页面滚动
  408. gameState.spaceShoot = true;
  409. }
  410. };
  411. const handleKeyUp = (event: any) => {
  412. if (event.key === 'ArrowLeft') {
  413. gameState.keyLeft = false;
  414. } else if (event.key === 'ArrowRight') {
  415. gameState.keyRight = false;
  416. } else if (event.key === ' ') { // 空格键释放
  417. gameState.spaceShoot = false;
  418. }
  419. };
  420. // 游戏方法
  421. const setupCanvas = () => {
  422. gameState.canvas = document.getElementById('canvas' + props.type);
  423. gameState.canvas.width = clientObj.value.width;
  424. gameState.canvas.height = clientObj.value.height;
  425. // gameState.canvas.width = document.documentElement.clientWidth / 2;
  426. // gameState.canvas.height = document.documentElement.clientHeight;
  427. gameState.ctx = gameState.canvas.getContext('2d');
  428. };
  429. const resizeToWindow = () => {
  430. // const w = gameState.canvas.width / gameState.canvas.height;
  431. // const h = window.innerHeight;
  432. // const ratio = h * w;
  433. // gameState.canvas.style.width = Math.floor(ratio) + 'px';
  434. // gameState.canvas.style.height = Math.floor(h) + 'px';
  435. };
  436. const drawLoadingScreen = () => {
  437. const ctx = gameState.ctx;
  438. ctx.fillStyle = 'black';
  439. ctx.fillRect(0, 0, 700, clientObj.value.width);
  440. ctx.textAlign = 'center';
  441. drawText(ctx, 'Loading...', clientObj.value.width / 2, 700 / 2, 40);
  442. ctx.textAlign = 'left';
  443. };
  444. const getResources = () => {
  445. const images = [
  446. '/static/images/basketball/background.png',
  447. '/static/images/basketball/title.png',
  448. '/static/images/basketball/ball.png',
  449. '/static/images/basketball/hoop.png'
  450. ];
  451. const sounds = [
  452. '/static/images/basketball/bounce_1.wav'
  453. ];
  454. return gameState.sound ? images.concat(sounds) : images;
  455. };
  456. const drawText = (ctx, string, x, y, size = 30) => {
  457. ctx.font = size + 'px Contrail One';
  458. ctx.lineWidth = 5;
  459. ctx.strokeStyle = 'white';
  460. ctx.strokeText(string, x, y);
  461. ctx.fillStyle = '#0098BF';
  462. ctx.fillText(string, x, y);
  463. };
  464. const playSound = (name) => {
  465. if (gameState.sound && gameState.res[name]) {
  466. gameState.res[name].currentTime = 0;
  467. gameState.res[name].play().catch(e => console.log('Sound play error:', e));
  468. }
  469. };
  470. const loadResources = () => {
  471. drawLoadingScreen();
  472. const resources = getResources();
  473. let loaded = 0;
  474. return new Promise((resolve) => {
  475. resources.forEach(resource => {
  476. const type = resource.split('.').pop();
  477. if (type === 'png') {
  478. const image = new Image();
  479. image.src = resource;
  480. image.addEventListener('load', () => {
  481. loaded++;
  482. gameState.res[resource] = image;
  483. if (loaded === resources.length) resolve();
  484. });
  485. } else if (['wav', 'mp3'].includes(type)) {
  486. const sound = new Audio();
  487. sound.src = resource;
  488. sound.addEventListener('canplaythrough', () => {
  489. loaded++;
  490. gameState.res[resource] = sound;
  491. if (loaded === resources.length) resolve();
  492. });
  493. }
  494. });
  495. });
  496. };
  497. const gameLoop = (timestamp) => {
  498. if (!gameState.then) gameState.then = timestamp;
  499. const delta = (timestamp - gameState.then) / 1000;
  500. // 更新游戏状态
  501. update(delta);
  502. // 绘制游戏
  503. draw();
  504. gameState.then = timestamp;
  505. gameState.animationFrameId = requestAnimationFrame(gameLoop);
  506. };
  507. const update = (delta) => {
  508. if (gameState.state === 'menu') {
  509. if (gameState.click) {
  510. gameState.state = 'play';
  511. gameState.click = false;
  512. }
  513. return;
  514. }
  515. if (gameState.state === 'play') {
  516. // // 更新篮球横向移动
  517. // gameState.ballX += gameState.ballVel * delta;
  518. // if (gameState.ballX > clientObj.value.width - 93) {
  519. // gameState.ballVel = -gameState.ballVel;
  520. // gameState.ballX = clientObj.value.width - 93;
  521. // }
  522. // if (gameState.ballX < 0) {
  523. // gameState.ballVel = -gameState.ballVel;
  524. // gameState.ballX = 0;
  525. // }
  526. // 添加篮筐更新逻辑
  527. gameState.hoops.forEach(hoop => {
  528. hoop.update(delta);
  529. });
  530. // 键盘控制篮球移动
  531. if (gameState.keyLeft) {
  532. //gameState.ballX -= gameState.ballVel * delta;
  533. gameState.ballX = direction.value;
  534. }
  535. if (gameState.keyRight) {
  536. //gameState.ballX += gameState.ballVel * delta;
  537. gameState.ballX = direction.value;
  538. }
  539. // 边界检查
  540. if (gameState.ballX > clientObj.value.width - 93) {
  541. gameState.ballX = clientObj.value.width - 93;
  542. }
  543. if (gameState.ballX < 0) {
  544. gameState.ballX = 0;
  545. }
  546. // 更新所有篮球
  547. for (let i = gameState.balls.length - 1; i >= 0; i--) {
  548. const ball = gameState.balls[i];
  549. if (ball.falling) {
  550. // 检测与篮筐的碰撞
  551. gameState.hoops.forEach(hoop => {
  552. // 篮筐中心计算(适配缩放后的尺寸)
  553. const scaledWidth = 148 * hoop.scale; // 原图宽度*缩放比例
  554. const cx = hoop.x + scaledWidth / 2;
  555. const cy = hoop.y + 40 * hoop.scale; // Y轴也适配缩放
  556. const dx = cx - ball.x;
  557. const dy = cy - ball.y;
  558. const mag = Math.sqrt(dx * dx + dy * dy);
  559. // 碰撞半径增大(适配放大后的篮筐)
  560. if (mag < 47 + 30 && !ball.scored) { // 从5→30
  561. ball.setAngle(90);
  562. gameState.score += 100;
  563. gameState.texts.push(new PopText('+ 100', hoop.x, hoop.y));
  564. ball.scored = true;
  565. }
  566. if (!ball.scored) {
  567. hoop.points.forEach(point => {
  568. const dx = point.x - ball.x;
  569. const dy = point.y - ball.y;
  570. const mag = Math.sqrt(dx * dx + dy * dy);
  571. // 调整碰撞检测阈值
  572. if (mag > 47 + 30 && !ball.canBounce) {
  573. ball.canBounce = true;
  574. }
  575. if (mag < 47 + 30 && ball.canBounce) {
  576. playSound('/static/images/basketball/bounce_1.wav');
  577. ball.bounces++;
  578. // 其他逻辑保持不变...
  579. }
  580. });
  581. }
  582. });
  583. }
  584. ball.update(delta);
  585. // 移除超出屏幕的球
  586. if (ball.y > clientObj.value.height) {
  587. gameState.ballX = ball.x;
  588. gameState.balls.splice(i, 1);
  589. }
  590. }
  591. // 更新计时器
  592. if (gameState.time > 0) {
  593. gameState.toNextSecond -= delta;
  594. if (gameState.toNextSecond <= 0) {
  595. gameState.time--;
  596. gameState.toNextSecond = 1;
  597. }
  598. } else {
  599. gameState.state = 'over';
  600. }
  601. // 处理投篮逻辑(修改后)
  602. const isShooting = gameState.click || gameState.spaceShoot; // 鼠标或键盘触发投篮
  603. if (isShooting && gameState.ballY <= clientObj.value.height) {
  604. if (gameState.balls.length < 1) {
  605. const ball = new Ball(gameState.ballX + (93 / 2), gameState.ballY);
  606. ball.drawAngle = gameState.ballAngle;
  607. // 使用计算好的力度投篮
  608. const shootPower = calculateShootPower();
  609. ball.shoot(shootPower);
  610. gameState.balls.push(ball);
  611. gameState.ballY = clientObj.value.height - 90;
  612. }
  613. }
  614. // 重置篮球位置
  615. if (gameState.balls.length < 1 && gameState.ballY > clientObj.value.height) {
  616. gameState.ballY -= 100 * delta;
  617. }
  618. if (!gameState.click) {
  619. gameState.ballsShot = 0;
  620. }
  621. // 更新弹出文字
  622. gameState.texts.forEach((text, i) => {
  623. text.update(delta);
  624. if (text.opacity <= 0) {
  625. gameState.texts.splice(i, 1);
  626. }
  627. });
  628. // 更新篮球角度
  629. gameState.ballAngle += 100 * delta;
  630. }
  631. // 游戏结束状态处理
  632. if (gameState.state === 'over' && gameState.click) {
  633. gameState.gameOver = false;
  634. gameState.started = false;
  635. gameState.score = 0;
  636. gameState.time = 60;
  637. gameState.balls = [];
  638. gameState.state = 'menu';
  639. gameState.click = false;
  640. }
  641. };
  642. /**
  643. * 计算确保超过篮筐的投篮力度
  644. * @returns 计算后的投篮力度
  645. */
  646. const calculateShootPower = () => {
  647. // 获取篮筐最高位置(取所有篮筐中最高的y坐标)
  648. const highestHoopY = Math.min(...gameState.hoops.map(hoop => hoop.y));
  649. // 计算需要超过篮筐的高度(额外增加200px安全距离)
  650. const requiredHeightAboveHoop = 200;
  651. // 计算从当前位置到目标高度的距离
  652. const distanceToTarget = gameState.ballY - (highestHoopY - requiredHeightAboveHoop);
  653. // 基础力度系数(可根据实际效果调整)
  654. const powerCoefficient = 4.3;
  655. // 最小力度保障(防止过小的屏幕尺寸导致力度不足)
  656. const minPower = clientObj.value.height;
  657. // 计算最终力度
  658. const calculatedPower = distanceToTarget * powerCoefficient;
  659. // 返回确保超过篮筐的力度(取计算值和最小值中的较大者)
  660. return Math.max(calculatedPower, minPower);
  661. };
  662. const draw = () => {
  663. const ctx = gameState.ctx;
  664. if (!ctx) return;
  665. // 绘制背景
  666. if (gameState.res['/static/images/basketball/background.png']) {
  667. const img = gameState.res['/static/images/basketball/background.png'];
  668. const canvasWidth = gameState.canvas.width;
  669. const canvasHeight = gameState.canvas.height;
  670. // 计算图片与画布的比例
  671. const imgRatio = img.width / img.height;
  672. const canvasRatio = canvasWidth / canvasHeight;
  673. let drawWidth, drawHeight, drawX = 0, drawY = 0;
  674. // 根据比例计算绘制尺寸
  675. if (canvasRatio > imgRatio) {
  676. // 画布更宽,按宽度缩放,高度可能超出
  677. drawWidth = canvasWidth;
  678. drawHeight = canvasWidth / imgRatio;
  679. drawY = (canvasHeight - drawHeight) / 2; // 垂直居中
  680. } else {
  681. // 画布更高,按高度缩放,宽度可能超出
  682. drawHeight = canvasHeight;
  683. drawWidth = canvasHeight * imgRatio;
  684. drawX = (canvasWidth - drawWidth) / 2; // 水平居中
  685. }
  686. // 绘制图片(可能裁剪边缘)
  687. ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
  688. }
  689. // 绘制菜单状态
  690. if (gameState.state === 'menu') {
  691. if (gameState.res['/static/images/basketball/title.png']) {
  692. ctx.drawImage(
  693. gameState.res['/static/images/basketball/title.png'],
  694. clientObj.value.width / 2 - (492 / 2),
  695. 100
  696. );
  697. }
  698. ctx.textAlign = 'center';
  699. drawText(ctx, '请做投篮动作开始游戏', clientObj.value.width / 2, 520, 40);
  700. }
  701. // 绘制游戏状态
  702. if (gameState.state === 'play') {
  703. // 绘制篮筐背景
  704. gameState.hoops.forEach(hoop => hoop.drawBack(ctx, gameState));
  705. // 绘制正在下落的球
  706. gameState.balls.forEach(ball => {
  707. if (ball.falling) ball.draw(ctx, gameState);
  708. });
  709. // 绘制篮筐前景
  710. gameState.hoops.forEach(hoop => hoop.drawFront(ctx, gameState));
  711. // 绘制未下落的球
  712. gameState.balls.forEach(ball => {
  713. if (!ball.falling) ball.draw(ctx, gameState);
  714. });
  715. // 绘制当前可投掷的球
  716. if (gameState.balls.length < 1 && gameState.res['/static/images/basketball/ball.png']) {
  717. drawImage(
  718. ctx,
  719. gameState.res['/static/images/basketball/ball.png'],
  720. gameState.ballX,
  721. gameState.ballY,
  722. 0, 0, 93, 93,
  723. 45, 45,
  724. gameState.ballAngle
  725. );
  726. }
  727. // 绘制分数和时间
  728. // 更靠左的得分
  729. drawText(ctx, '得分: ' + gameState.score, 150, 70, 50);
  730. // 更靠右的时间
  731. drawText(ctx, '时间: ' + gameState.time, clientObj.value.width - 115, 70, 50);
  732. // 绘制弹出文字
  733. gameState.texts.forEach(text => text.draw(ctx, { drawText }));
  734. }
  735. // 绘制游戏结束界面
  736. if (gameState.state === 'over') {
  737. ctx.textAlign = 'center';
  738. drawText(ctx, 'Game Over', clientObj.value.width / 2, 200, 80);
  739. drawText(ctx, '成绩:' + gameState.score, clientObj.value.width / 2, 400, 50);
  740. ctx.textAlign = 'center';
  741. // 游戏结束后2秒自动退出
  742. if (!gameState.exitTimer) {
  743. gameState.exitTimer = setTimeout(() => {
  744. // 退出游戏,这里可以根据实际需求调整,比如返回主菜单或跳转到其他页面
  745. gameState.gameOver = false;
  746. gameState.started = false;
  747. gameState.score = 0;
  748. gameState.time = 60;
  749. gameState.balls = [];
  750. //gameState.state = 'menu';
  751. gameState.click = false;
  752. gameState.exitTimer = null;
  753. // 如果需要跳转到其他页面,可以使用路由
  754. // router.push('/');
  755. emit('confirmExit', { type: 2, area: props.currentGameArea });
  756. }, 500);
  757. }
  758. // 保留点击退出的功能
  759. if (gameState.click) {
  760. clearTimeout(gameState.exitTimer);
  761. gameState.gameOver = false;
  762. gameState.started = false;
  763. gameState.score = 0;
  764. gameState.time = 60;
  765. gameState.balls = [];
  766. gameState.state = 'menu';
  767. gameState.click = false;
  768. gameState.exitTimer = null;
  769. }
  770. }
  771. };
  772. // 初始化游戏
  773. const initGame = async () => {
  774. setupCanvas();
  775. resizeToWindow();
  776. window.addEventListener('resize', resizeToWindow);
  777. // 添加键盘事件监听
  778. window.addEventListener('keydown', handleKeyDown);
  779. window.addEventListener('keyup', handleKeyUp);
  780. await loadResources();
  781. // 添加篮筐
  782. gameState.hoops = [
  783. //new Hoop(110, 300),
  784. // 计算居中位置(考虑放大后的宽度148*1.5≈222)
  785. new Hoop((clientObj.value.width - 222) / 2, 150),
  786. //new Hoop(clientObj.value.width - 148 - 110, 300),
  787. ];
  788. // 开始游戏循环
  789. gameState.animationFrameId = requestAnimationFrame(gameLoop);
  790. gameState.ballY = clientObj.value.height - 90;
  791. setTimeout(() => {
  792. handleMouseDown();
  793. setTimeout(() => {
  794. handleMouseUp();
  795. }, 100)
  796. }, 1000)
  797. };
  798. onBeforeMount(() => {
  799. });
  800. // 生命周期钩子
  801. onMounted(() => {
  802. clientObj.value = {
  803. width: document.querySelector('.game-container').offsetWidth,
  804. height: document.querySelector('.game-container').offsetHeight,
  805. }
  806. let obj: any = props.areaStateList.find((item: any) => {
  807. return item.area == props.currentGameArea
  808. })
  809. boxes.value = obj.boxes;
  810. proportion.value = (clientObj.value.height / (boxes.value[0].y - boxes.value[1].y)).toFixed(2);
  811. initGame();
  812. });
  813. onBeforeUnmount(() => {
  814. if (gameState.animationFrameId) {
  815. cancelAnimationFrame(gameState.animationFrameId);
  816. }
  817. // 清除退出定时器
  818. if (gameState.exitTimer) {
  819. clearTimeout(gameState.exitTimer);
  820. }
  821. window.removeEventListener('resize', resizeToWindow);
  822. window.removeEventListener('keydown', handleKeyDown);
  823. window.removeEventListener('keyup', handleKeyUp);
  824. });
  825. //暴露给父组件用
  826. defineExpose({
  827. getInit
  828. })
  829. </script>
  830. <style scoped>
  831. .game-container {
  832. width: 100%;
  833. height: 100vh;
  834. display: flex;
  835. justify-content: center;
  836. align-items: center;
  837. background: #000000;
  838. overflow: hidden;
  839. position: relative;
  840. }
  841. #canvas {
  842. display: block;
  843. touch-action: none;
  844. user-select: none;
  845. }
  846. </style>