123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744 |
- <template>
- <div class="game-container">
- <canvas id="canvas" @mousedown="handleMouseDown" @mouseup="handleMouseUp" @touchstart="handleTouchStart"
- @touchend="handleTouchEnd"></canvas>
- </div>
- </template>
- <script setup name="Basketball" lang="ts">
- import { onMounted, ref, reactive, onBeforeUnmount } from 'vue';
- import { initSpeech, speckText, playMusic, controlMusic, speckCancel, chineseNumber } from '@/utils/speech';
- import { useWebSocket } from '@/utils/bodyposeWs';
- const { proxy } = getCurrentInstance() as any;
- const router = useRouter();
- const { bodyposeWs, startDevice, checkBodypose, openBodypose, terminateBodypose, suspendBodypose, resumeBodypose, getBodyposeState, closeWS } = useWebSocket();
- const data = reactive<any>({
- bodyposeData: {},//姿态信息
- bodyposeState: false,//姿态识别窗口状态
- parameter: {},//参数
- deviceInfo: {},//设备信息
- againNum: 0,//再次启动次数
- againTimer: null,//定时状态
- wsState: false,//WS状态
- clientObj: {},//浏览器对象
- myThrow: 0,//0放下1举投
- });
- const { bodyposeData, bodyposeState, parameter, deviceInfo, againNum, againTimer, wsState, clientObj, myThrow } = toRefs(data);
- /**
- * 输出犯规
- */
- watch(
- () => myThrow.value,
- (newData,oldData) => {
- console.log("ppp",oldData,newData)
- if (newData == 1) {
- console.log("投篮")
- handleMouseDown();
- setTimeout(()=>{
- handleMouseUp();
- },100)
-
- }else{
-
- }
- },
- { immediate: true }
- );
- /**
- * 初始化
- */
- const getInit = async () => {
- console.log("触发姿态识别")
- let deviceid = localStorage.getItem('deviceid') || '';
- if (!deviceid) {
- proxy?.$modal.msgError(`请重新登录绑定设备号后使用`);
- return false;
- }
- bodyposeState.value = true;
- if (wsState.value) {
- proxy?.$modal.msgWarning(`操作过快,请稍后重试`);
- setTimeout(() => {
- bodyposeState.value = false;
- }, 1000)
- return false;
- }
- speckText("正在姿态识别");
- bodyposeWs((e: any) => {
- //console.log("bodyposeWS", e)
- if (e?.wksid) {
- wsState.value = true;
- //获取设备信息
- startDevice({ deviceid: deviceid });
- console.log("获取设备信息")
- }
- if (e?.type == 'fe_device_init_result') {
- //接收设备信息并发送请求
- if (e?.device_info) {
- deviceInfo.value = e.device_info;
- getCheckBodypose();
- console.log("返回设备信息,检查是否支持姿态识别")
- } else {
- proxy?.$modal.msgError(`设备信息缺失,请重新登录绑定设备号后使用`);
- }
- }
- if (e?.cmd == 'check_bodyposecontroller_available') {
- let handcontroller_id = deviceInfo.value.handcontroller_id;
- if (e?.code == 0) {
- //查看姿态识别状态,如果不处于关闭就先关闭再重新启动(可能会APP退出然后工作站还在运行的可能性)
- getBodyposeState(handcontroller_id);
- againNum.value = 0;
- againTimer.value = null;
- clearTimeout(againTimer.value);
- console.log("查看姿态识别状态")
- } else {
- //尝试多次查询姿态识别状态
- if (againNum.value <= 2) {
- againTimer.value = setTimeout(() => {
- getCheckBodypose();
- }, 500)
- againNum.value++;
- } else {
- let msg = "";
- if (e.code == 102402) {
- msg = `多次连接失败请重试,姿态识别模块被占用`;
- } else {
- msg = `多次连接失败请重试,姿态识别模块不可用,code:${e.code}`;
- }
- proxy?.$modal.msgWarning(msg);
- againNum.value = 0;
- againTimer.value = null;
- clearTimeout(againTimer.value);
- getCloseBodypose();//直接关闭
- }
- }
- }
- if (e?.cmd == 'get_bodyposecontroller_state') {
- let handcontroller_id = deviceInfo.value.handcontroller_id;
- //state说明: 0:关闭,3:空闲,36:工作中
- if ([3, 36].includes(e.state)) {
- getCloseBodypose();
- proxy?.$modal.msgWarning(`请重新姿态识别`);
- } else {
- openBodypose(handcontroller_id);
- }
- }
- if (e?.type == 'bodyposecontroller_result') {
- let arr = e.data.result.keypoints;
- let result = [];
- for (let i = 0; i < arr.length; i += 3) {
- result.push(arr.slice(i, i + 2));
- }
- //console.log("result", result)
- bodyposeData.value = result;
- getCanvas(result);
- }
- if (e?.cmd == 'terminate_bodyposecontroller') {
- if (e?.code == 0) {
- closeWS();
- bodyposeState.value = false;
- }
- }
- if (e?.type == 'disconnect') {
- wsState.value = false;
- }
- });
- };
- /**
- * 查询姿态识别状态
- */
- const getCheckBodypose = () => {
- let handcontroller_id = deviceInfo.value.handcontroller_id;
- //检查是否支持姿态识别
- checkBodypose(handcontroller_id);
- };
- /**
- * 关闭姿态识别
- */
- const getCloseBodypose = () => {
- let handcontroller_id = deviceInfo.value.handcontroller_id;
- terminateBodypose(handcontroller_id);
- bodyposeState.value = false;
- speckCancel(); //停止播报
- setTimeout(() => {
- if (wsState.value) {
- closeWS();
- }
- }, 3000)
- };
- const getCanvas = (data) => {
- let leftA = data[6][1];
- let rightA = data[5][1];
- let leftB = data[10][1];
- let rightB = data[9][1];
- if (leftB > leftA || rightB > rightA) {
- myThrow.value = 1;
- } else {
- myThrow.value = 0;
- }
- };
- // 游戏主类的响应式状态
- const gameState = reactive({
- version: '0.1',
- balls: [],
- hoops: [],
- texts: [],
- res: {},
- score: 0,
- started: false,
- gameOver: false,
- ballX: 320 / 2,
- ballY: 880,
- ballVel: 300,
- ballAngleVel: 100,
- ballAngle: 0,
- ballsShot: 1,
- ballCharge: 0,
- time: 60,
- toNextSecond: 1,
- sound: false,
- state: 'menu',
- click: false,
- canvas: null,
- ctx: null,
- animationFrameId: null,
- then: 0
- });
- // 篮筐类
- class Hoop {
- constructor(x, y) {
- this.x = x;
- this.y = y;
- this.points = [
- { x: x + 7, y: y + 18 },
- { x: x + 141, y: y + 18 }
- ];
- }
- drawBack(ctx, game) {
- drawImage(
- ctx,
- game.res['/static/images/basketball/hoop.png'],
- this.x,
- this.y,
- 0, 0, 148, 22, 0, 0, 0
- );
- }
- drawFront(ctx, game) {
- drawImage(
- ctx,
- game.res['/static/images/basketball/hoop.png'],
- this.x,
- this.y + 22,
- 0, 22, 148, 178 - 22, 0, 0, 0
- );
- }
- }
- // 篮球类
- class Ball {
- constructor(x, y) {
- this.x = x;
- this.y = y;
- this.vx = 0;
- this.vy = 0;
- this.speed = 100;
- this.canBounce = true;
- this.angle = 270;
- this.gravity = 0;
- this.falling = false;
- this.bounces = 0;
- this.scored = false;
- this.drawAngle = 0;
- this.angleVel = 100;
- this.solid = false;
- this.z = 1;
- }
-
- setAngle(angle) {
- this.angle = angle;
- this.vx = this.speed * Math.cos(this.angle * Math.PI / 180);
- this.vy = this.speed * Math.sin(this.angle * Math.PI / 180);
- this.gravity = 0;
- }
- shoot(power) {
- this.speed = power + Math.floor(Math.random() * 40);
- this.setAngle(270);
- }
- update(delta) {
- this.y += this.gravity * delta;
- this.gravity += 1500 * delta;
- this.x += this.vx * delta;
- this.y += this.vy * delta;
- if (this.vx > 500) this.vx = 500;
- if (this.vy > 500) this.vy = 500;
- if (this.y < 300) {
- this.solid = true;
- }
- if (this.gravity > this.speed) {
- this.falling = true;
- }
- if (this.x + 47 > 640) {
- this.vx = this.vx * -1;
- this.x = 640 - 47;
- }
- if (this.x - 47 < 0) {
- this.vx = this.vx * -1;
- this.x = 47;
- }
- this.drawAngle += this.angleVel * delta;
- }
- draw(ctx, game) {
- drawImage(
- ctx,
- game.res['/static/images/basketball/ball.png'],
- Math.floor(this.x - (93 / 2)),
- Math.floor(this.y - (93 / 2)),
- 0, 0, 93, 93, 93 / 2, 93 / 2,
- this.drawAngle
- );
- }
- }
- // 弹出文字类
- class PopText {
- constructor(string, x, y) {
- this.string = string;
- this.x = x;
- this.y = y;
- this.vy = -500;
- this.opacity = 1;
- }
- update(delta) {
- this.y += this.vy * delta;
- this.vy += 1000 * delta;
- if (this.vy > 0 && this.opacity > 0) {
- this.opacity -= 2 * delta;
- }
- if (this.opacity <= 0) {
- this.opacity = 0;
- }
- }
- draw(ctx, game) {
- ctx.globalAlpha = this.opacity;
- game.drawText(ctx, this.string, this.x + 15, this.y);
- ctx.globalAlpha = 1;
- }
- }
- // 工具函数:绘制旋转图像
- function drawImage(ctx, image, x, y, sx, sy, w, h, rx, ry, a) {
- ctx.save();
- ctx.translate(x + rx, y + ry);
- ctx.rotate(a * Math.PI / 180);
- ctx.drawImage(image, sx, sy, w, h, -rx, -ry, w, h);
- ctx.restore();
- }
- // 事件处理函数
- const handleMouseDown = () => {
- gameState.click = true;
- };
- const handleMouseUp = () => {
- gameState.click = false;
- };
- const handleTouchStart = () => {
- gameState.click = true;
- };
- const handleTouchEnd = () => {
- gameState.click = false;
- };
- // 游戏方法
- const setupCanvas = () => {
- gameState.canvas = document.getElementById('canvas');
- gameState.canvas.width = 640;
- gameState.canvas.height = 960;
- gameState.ctx = gameState.canvas.getContext('2d');
- };
- const resizeToWindow = () => {
- const w = gameState.canvas.width / gameState.canvas.height;
- const h = window.innerHeight;
- const ratio = h * w;
- gameState.canvas.style.width = Math.floor(ratio) + 'px';
- gameState.canvas.style.height = Math.floor(h) + 'px';
- };
- const drawLoadingScreen = () => {
- const ctx = gameState.ctx;
- ctx.fillStyle = 'black';
- ctx.fillRect(0, 0, 960, 640);
- ctx.textAlign = 'center';
- drawText(ctx, 'Loading...', 640 / 2, 960 / 2, 40);
- ctx.textAlign = 'left';
- };
- const getResources = () => {
- const images = [
- '/static/images/basketball/background.png',
- '/static/images/basketball/title.png',
- '/static/images/basketball/ball.png',
- '/static/images/basketball/hoop.png'
- ];
- const sounds = [
- '/static/images/basketball/bounce_1.wav'
- ];
- return gameState.sound ? images.concat(sounds) : images;
- };
- const drawText = (ctx, string, x, y, size = 30) => {
- ctx.font = size + 'px Contrail One';
- ctx.lineWidth = 5;
- ctx.strokeStyle = 'white';
- ctx.strokeText(string, x, y);
- ctx.fillStyle = '#0098BF';
- ctx.fillText(string, x, y);
- };
- const playSound = (name) => {
- if (gameState.sound && gameState.res[name]) {
- gameState.res[name].currentTime = 0;
- gameState.res[name].play().catch(e => console.log('Sound play error:', e));
- }
- };
- const loadResources = () => {
- drawLoadingScreen();
- const resources = getResources();
- let loaded = 0;
- return new Promise((resolve) => {
- resources.forEach(resource => {
- const type = resource.split('.').pop();
- if (type === 'png') {
- const image = new Image();
- image.src = resource;
- image.addEventListener('load', () => {
- loaded++;
- gameState.res[resource] = image;
- if (loaded === resources.length) resolve();
- });
- } else if (['wav', 'mp3'].includes(type)) {
- const sound = new Audio();
- sound.src = resource;
- sound.addEventListener('canplaythrough', () => {
- loaded++;
- gameState.res[resource] = sound;
- if (loaded === resources.length) resolve();
- });
- }
- });
- });
- };
- const gameLoop = (timestamp) => {
- if (!gameState.then) gameState.then = timestamp;
- const delta = (timestamp - gameState.then) / 1000;
- // 更新游戏状态
- update(delta);
- // 绘制游戏
- draw();
- gameState.then = timestamp;
- gameState.animationFrameId = requestAnimationFrame(gameLoop);
- };
- const update = (delta) => {
- if (gameState.state === 'menu') {
- if (gameState.click) {
- gameState.state = 'play';
- gameState.click = false;
- }
- return;
- }
- if (gameState.state === 'play') {
- // 更新篮球横向移动
- gameState.ballX += gameState.ballVel * delta;
- if (gameState.ballX > 640 - 93) {
- gameState.ballVel = -gameState.ballVel;
- gameState.ballX = 640 - 93;
- }
- if (gameState.ballX < 0) {
- gameState.ballVel = -gameState.ballVel;
- gameState.ballX = 0;
- }
- // 更新所有篮球
- for (let i = gameState.balls.length - 1; i >= 0; i--) {
- const ball = gameState.balls[i];
- if (ball.falling) {
- // 检测与篮筐的碰撞
- gameState.hoops.forEach(hoop => {
- const cx = hoop.x + (148 / 2);
- const cy = hoop.y + 40;
- const dx = cx - ball.x;
- const dy = cy - ball.y;
- const mag = Math.sqrt(dx * dx + dy * dy);
- if (mag < 47 + 5 && !ball.scored) {
- ball.setAngle(90);
- gameState.score += 100;
- gameState.texts.push(new PopText('+ 100', hoop.x, hoop.y));
- ball.scored = true;
- }
- if (!ball.scored) {
- hoop.points.forEach(point => {
- const dx = point.x - ball.x;
- const dy = point.y - ball.y;
- const mag = Math.sqrt(dx * dx + dy * dy);
- const angle = Math.atan2(point.y - ball.y, point.x - ball.x);
- if (mag > 47 + 7 && !ball.canBounce) {
- ball.canBounce = true;
- }
- if (mag < 47 + 5 && ball.canBounce) {
- playSound('/static/images/basketball/bounce_1.wav');
- ball.bounces++;
- ball.setAngle((angle * 180 / Math.PI) + 180 + Math.floor(Math.random() * 5) - Math.floor(Math.random() * 5));
- ball.bounces = Math.min(ball.bounces, 3);
- const deg = angle * 180 / Math.PI;
- if (deg > 0 && deg < 180) {
- ball.gravity = 750 + (ball.bounces * 50);
- }
- ball.angleVel = -ball.angleVel;
- ball.canBounce = false;
- }
- });
- }
- });
- }
- ball.update(delta);
- // 移除超出屏幕的球
- if (ball.y > 960) {
- gameState.ballX = ball.x;
- gameState.balls.splice(i, 1);
- }
- }
- // 更新计时器
- if (gameState.time > 0) {
- gameState.toNextSecond -= delta;
- if (gameState.toNextSecond <= 0) {
- gameState.time--;
- gameState.toNextSecond = 1;
- }
- } else {
- gameState.state = 'over';
- }
- // 处理投篮逻辑
- if (gameState.click && gameState.ballY <= 950) {
- if (gameState.balls.length < 1) {
- const ball = new Ball(gameState.ballX + (93 / 2), gameState.ballY);
- ball.drawAngle = gameState.ballAngle;
- ball.shoot(1480);
- gameState.balls.push(ball);
- gameState.ballY = 961;
- }
- }
- // 重置篮球位置
- if (gameState.balls.length < 1 && gameState.ballY > 880) {
- gameState.ballY -= 100 * delta;
- }
- if (!gameState.click) {
- gameState.ballsShot = 0;
- }
- // 更新弹出文字
- gameState.texts.forEach((text, i) => {
- text.update(delta);
- if (text.opacity <= 0) {
- gameState.texts.splice(i, 1);
- }
- });
- // 更新篮球角度
- gameState.ballAngle += 100 * delta;
- }
- // 游戏结束状态处理
- if (gameState.state === 'over' && gameState.click) {
- gameState.gameOver = false;
- gameState.started = false;
- gameState.score = 0;
- gameState.time = 60;
- gameState.balls = [];
- gameState.state = 'menu';
- gameState.click = false;
- }
- };
- const draw = () => {
- const ctx = gameState.ctx;
- if (!ctx) return;
- // 绘制背景
- if (gameState.res['/static/images/basketball/background.png']) {
- ctx.drawImage(gameState.res['/static/images/basketball/background.png'], 0, 0);
- }
- // 绘制菜单状态
- if (gameState.state === 'menu') {
- if (gameState.res['/static/images/basketball/title.png']) {
- ctx.drawImage(
- gameState.res['/static/images/basketball/title.png'],
- 640 / 2 - (492 / 2),
- 100
- );
- }
- ctx.textAlign = 'center';
- drawText(ctx, '请做投篮动作开始游戏', 640 / 2, 520, 40);
- drawText(ctx, '(?) Tap to throw ball. Try to make as many hoops before the time runs out!', 640 / 2, 940, 20);
- ctx.textAlign = 'left';
- }
- // 绘制游戏状态
- if (gameState.state === 'play') {
- // 绘制篮筐背景
- gameState.hoops.forEach(hoop => hoop.drawBack(ctx, gameState));
- // 绘制正在下落的球
- gameState.balls.forEach(ball => {
- if (ball.falling) ball.draw(ctx, gameState);
- });
- // 绘制篮筐前景
- gameState.hoops.forEach(hoop => hoop.drawFront(ctx, gameState));
- // 绘制未下落的球
- gameState.balls.forEach(ball => {
- if (!ball.falling) ball.draw(ctx, gameState);
- });
- // 绘制当前可投掷的球
- if (gameState.balls.length < 1 && gameState.res['/static/images/basketball/ball.png']) {
- drawImage(
- ctx,
- gameState.res['/static/images/basketball/ball.png'],
- gameState.ballX,
- gameState.ballY,
- 0, 0, 93, 93,
- 45, 45,
- gameState.ballAngle
- );
- }
- // 绘制分数和时间
- drawText(ctx, 'Score: ' + gameState.score, 45, 70, 50);
- drawText(ctx, 'Time: ' + gameState.time, 435, 70, 50);
- // 绘制弹出文字
- gameState.texts.forEach(text => text.draw(ctx, { drawText }));
- }
- // 绘制游戏结束界面
- if (gameState.state === 'over') {
- ctx.textAlign = 'center';
- drawText(ctx, 'Game Over', 640 / 2, 200, 80);
- drawText(ctx, 'Score: ' + gameState.score, 640 / 2, 400, 50);
- drawText(ctx, 'Click to Continue', 640 / 2, 800, 50);
- ctx.textAlign = 'center';
- }
- };
- // 初始化游戏
- const initGame = async () => {
- setupCanvas();
- resizeToWindow();
- window.addEventListener('resize', resizeToWindow);
- await loadResources();
- // 添加篮筐
- gameState.hoops = [
- new Hoop(110, 520),
- new Hoop(640 - 148 - 110, 520),
- new Hoop(640 / 2 - (148 / 2), 260)
- ];
- // 开始游戏循环
- gameState.animationFrameId = requestAnimationFrame(gameLoop);
- };
- // 生命周期钩子
- onMounted(() => {
- initGame();
- });
- onBeforeMount(() => {
- getInit();
- });
- onBeforeUnmount(() => {
- closeWS();
- if (gameState.animationFrameId) {
- cancelAnimationFrame(gameState.animationFrameId);
- }
- window.removeEventListener('resize', resizeToWindow);
- });
- </script>
- <style scoped>
- .game-container {
- width: 100vw;
- height: 100vh;
- display: flex;
- justify-content: center;
- align-items: center;
- background-color: #000;
- overflow: hidden;
- }
- #canvas {
- display: block;
- touch-action: none;
- user-select: none;
- }
- </style>
|