|
@@ -1,7 +1,8 @@
|
|
|
<template>
|
|
|
<div class="game-container">
|
|
|
<div id="gameCanvas" class="game-canvas"></div>
|
|
|
-
|
|
|
+ <canvas ref="canvasRef" :width="clientObj.width" :height="clientObj.height"
|
|
|
+ style="position:fixed;left: 0; top: 0; z-index: 999;"></canvas>
|
|
|
<!-- 游戏启动界面 -->
|
|
|
<div v-if="currentScene === 'start'" class="gamestart">
|
|
|
<img v-if="currentScene === 'start'" src="/static/images/football/game_start.jpg" class="start_bg" />
|
|
@@ -58,9 +59,34 @@
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
-<script setup>
|
|
|
-import { ref, onMounted, watch } from 'vue';
|
|
|
+<script setup name="Football" lang="ts">
|
|
|
import Phaser from 'phaser';
|
|
|
+import { onMounted, ref, reactive, onBeforeUnmount, watch } 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 canvasRef = ref(null);
|
|
|
+
|
|
|
+
|
|
|
+const data = reactive<any>({
|
|
|
+ bodyposeData: {},//姿态信息
|
|
|
+ bodyposeState: false,//姿态识别窗口状态
|
|
|
+ parameter: {},//参数
|
|
|
+ deviceInfo: {},//设备信息
|
|
|
+ againNum: 0,//再次启动次数
|
|
|
+ againTimer: null,//定时状态
|
|
|
+ wsState: false,//WS状态
|
|
|
+ clientObj: {},//浏览器对象
|
|
|
+ boxes: [],//四个点坐标
|
|
|
+ proportion: null,//人框和屏幕比例
|
|
|
+ myThrow: 0,//0踢腿 1收腿
|
|
|
+ myTimer: null,
|
|
|
+ direction: null,//跑动
|
|
|
+});
|
|
|
+
|
|
|
+const { bodyposeData, bodyposeState, parameter, deviceInfo, againNum, againTimer, wsState, clientObj, boxes, proportion, myThrow, myTimer, direction } = toRefs(data);
|
|
|
|
|
|
// 游戏状态管理
|
|
|
const currentScene = ref('start');
|
|
@@ -677,7 +703,7 @@ class GameScene extends Phaser.Scene {
|
|
|
this.tweens.add({
|
|
|
targets: this.goalkeeper,
|
|
|
x: [GAME_WIDTH / 2 - 50, GAME_WIDTH / 2 + 50],
|
|
|
- duration: 2000,
|
|
|
+ duration: 4000,
|
|
|
ease: 'Sine.inOut',
|
|
|
repeat: -1,
|
|
|
yoyo: true
|
|
@@ -1042,6 +1068,356 @@ watch(currentScene, (newVal) => {
|
|
|
}
|
|
|
});
|
|
|
|
|
|
+/**
|
|
|
+ * 监听投篮
|
|
|
+ */
|
|
|
+watch(
|
|
|
+ () => myThrow.value,
|
|
|
+ (newData, oldData) => {
|
|
|
+ if (myTimer.value) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ console.log("ppp", oldData, newData)
|
|
|
+ if (newData == 1) {
|
|
|
+ console.log("收腿准备")
|
|
|
+ } else {
|
|
|
+ console.log("踢出去了")
|
|
|
+ myTimer.value = setTimeout(() => {
|
|
|
+ clearTimeout(myTimer.value);
|
|
|
+ myTimer.value = null;
|
|
|
+ }, 1000)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+);
|
|
|
+
|
|
|
+/**
|
|
|
+ * 监听跑动
|
|
|
+ */
|
|
|
+watch(
|
|
|
+ () => direction.value,
|
|
|
+ (newData, oldData) => {
|
|
|
+ nextTick(() => {
|
|
|
+ if (newData > oldData) {
|
|
|
+
|
|
|
+ } 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("正在姿态识别");
|
|
|
+ proxy?.$modal.msgWarning(`正在姿态识别`);
|
|
|
+ bodyposeWs((e: any) => {
|
|
|
+ //console.log("bodyposeWS", e)
|
|
|
+ if (e?.wksid) {
|
|
|
+ wsState.value = true;
|
|
|
+ //获取设备信息
|
|
|
+ startDevice({ deviceid: deviceid });
|
|
|
+ console.log("获取设备信息")
|
|
|
+ }
|
|
|
+ if (e?.type == 'fe_device_init_result') {
|
|
|
+ //接收设备信息并发送请求
|
|
|
+ if (e?.device_info) {
|
|
|
+ deviceInfo.value = e.device_info;
|
|
|
+ getCheckBodypose();
|
|
|
+ console.log("返回设备信息,检查是否支持姿态识别")
|
|
|
+ } else {
|
|
|
+ proxy?.$modal.msgError(`设备信息缺失,请重新登录绑定设备号后使用`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (e?.cmd == 'check_bodyposecontroller_available') {
|
|
|
+ let handcontroller_id = deviceInfo.value.handcontroller_id;
|
|
|
+ if (e?.code == 0) {
|
|
|
+ //查看姿态识别状态,如果不处于关闭就先关闭再重新启动(可能会APP退出然后工作站还在运行的可能性)
|
|
|
+ getBodyposeState(handcontroller_id);
|
|
|
+ againNum.value = 0;
|
|
|
+ againTimer.value = null;
|
|
|
+ clearTimeout(againTimer.value);
|
|
|
+ console.log("查看姿态识别状态")
|
|
|
+ } else {
|
|
|
+ //尝试多次查询姿态识别状态
|
|
|
+ if (againNum.value <= 2) {
|
|
|
+ againTimer.value = setTimeout(() => {
|
|
|
+ getCheckBodypose();
|
|
|
+ }, 500)
|
|
|
+ againNum.value++;
|
|
|
+ } else {
|
|
|
+ let msg = "";
|
|
|
+ if (e.code == 102402) {
|
|
|
+ msg = `多次连接失败请重试,姿态识别模块被占用`;
|
|
|
+ } else {
|
|
|
+ msg = `多次连接失败请重试,姿态识别模块不可用,code:${e.code}`;
|
|
|
+ }
|
|
|
+ proxy?.$modal.msgWarning(msg);
|
|
|
+ againNum.value = 0;
|
|
|
+ againTimer.value = null;
|
|
|
+ clearTimeout(againTimer.value);
|
|
|
+ getCloseBodypose();//直接关闭
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (e?.cmd == 'get_bodyposecontroller_state') {
|
|
|
+ let handcontroller_id = deviceInfo.value.handcontroller_id;
|
|
|
+ //state说明: 0:关闭,3:空闲,36:工作中
|
|
|
+ if ([3, 36].includes(e.state)) {
|
|
|
+ getCloseBodypose();
|
|
|
+ proxy?.$modal.msgWarning(`请重新姿态识别`);
|
|
|
+ } else {
|
|
|
+ openBodypose(handcontroller_id);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (e?.type == 'bodyposecontroller_result') {
|
|
|
+ let arr = e.data.result.keypoints;
|
|
|
+ let result = [];
|
|
|
+ for (let i = 0; i < arr.length; i += 3) {
|
|
|
+ result.push(arr.slice(i, i + 2));
|
|
|
+ }
|
|
|
+ //console.log("result", result)
|
|
|
+ bodyposeData.value = result;
|
|
|
+ if (boxes.value.length == 0) {
|
|
|
+ speckText("识别成功");
|
|
|
+ proxy?.$modal.msgWarning(`识别成功`);
|
|
|
+ let arr = e.data.result.boxes;
|
|
|
+ boxes.value = [{ x: arr[0], y: arr[3] }, { x: arr[0], y: arr[1] }, { x: arr[2], y: arr[1] }, { x: arr[2], y: arr[3] }]
|
|
|
+ proportion.value = (clientObj.value.height / (arr[3] - arr[1])).toFixed(2);
|
|
|
+ }
|
|
|
+ getCanvas();
|
|
|
+ }
|
|
|
+ if (e?.cmd == 'terminate_bodyposecontroller') {
|
|
|
+ if (e?.code == 0) {
|
|
|
+ closeWS();
|
|
|
+ bodyposeState.value = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (e?.type == 'disconnect') {
|
|
|
+ wsState.value = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+
|
|
|
+/**
|
|
|
+ * 查询姿态识别状态
|
|
|
+*/
|
|
|
+const getCheckBodypose = () => {
|
|
|
+ let handcontroller_id = deviceInfo.value.handcontroller_id;
|
|
|
+ //检查是否支持姿态识别
|
|
|
+ checkBodypose(handcontroller_id);
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 关闭姿态识别
|
|
|
+*/
|
|
|
+const getCloseBodypose = () => {
|
|
|
+ let handcontroller_id = deviceInfo.value.handcontroller_id;
|
|
|
+ terminateBodypose(handcontroller_id);
|
|
|
+ bodyposeState.value = false;
|
|
|
+ speckCancel(); //停止播报
|
|
|
+ setTimeout(() => {
|
|
|
+ if (wsState.value) {
|
|
|
+ closeWS();
|
|
|
+ }
|
|
|
+ }, 3000)
|
|
|
+};
|
|
|
+
|
|
|
+
|
|
|
+const getCanvas = () => {
|
|
|
+
|
|
|
+ let leftA = { x: bodyposeData.value[12][0], y: bodyposeData.value[12][1] };//大腿
|
|
|
+ let leftB = { x: bodyposeData.value[14][0], y: bodyposeData.value[14][1] };//膝盖
|
|
|
+ let leftC = { x: bodyposeData.value[16][0], y: bodyposeData.value[16][1] };//脚
|
|
|
+
|
|
|
+ let rightA = { x: bodyposeData.value[11][0], y: bodyposeData.value[11][1] };//大腿
|
|
|
+ let rightB = { x: bodyposeData.value[13][0], y: bodyposeData.value[13][1] };//膝盖
|
|
|
+ let rightC = { x: bodyposeData.value[15][0], y: bodyposeData.value[15][1] };//脚
|
|
|
+
|
|
|
+ let jiaodu1 = calculateAngleAtB(leftA, leftB, leftC)
|
|
|
+ let jiaodu2 = calculateAngleAtB(rightA, rightB, rightC)
|
|
|
+ // console.log("jiaodu1",jiaodu1)
|
|
|
+ // console.log("jiaodu2",jiaodu2)
|
|
|
+ if (jiaodu1 <= 80 && jiaodu2 >= 120 || jiaodu2 <= 80 && jiaodu2 >= 120) {
|
|
|
+ myThrow.value = 1;
|
|
|
+ } else {
|
|
|
+ myThrow.value = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ const canvas: any = canvasRef.value;
|
|
|
+ const ctx = canvas.getContext('2d');
|
|
|
+ // 清空整个画布
|
|
|
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
+ // 保存当前状态
|
|
|
+ ctx.save();
|
|
|
+ function calculateOffset(a: any, b: any) {
|
|
|
+ return {
|
|
|
+ x: b.x - a.x,
|
|
|
+ y: b.y - a.y
|
|
|
+ };
|
|
|
+ }
|
|
|
+ const pointA = { x: clientObj.value.width / 2, y: clientObj.value.height / 2 };
|
|
|
+ const pointB = { x: (boxes.value[2].x + boxes.value[0].x) / 2, y: (boxes.value[3].y + boxes.value[1].y) / 2 };
|
|
|
+ const offset = calculateOffset(pointA, pointB);
|
|
|
+ ctx.translate(-offset.x, -offset.y);
|
|
|
+ // console.log("Canvas分辨率", clientObj.value);
|
|
|
+ // console.log("人体图片四点坐标", boxes.value)
|
|
|
+ // console.log("Canvas中心", pointA);
|
|
|
+ // console.log("人体中心", pointB);
|
|
|
+ // console.log("offset", offset)
|
|
|
+ // console.log("proportion.value",proportion.value)
|
|
|
+ const originalPoints = bodyposeData.value;
|
|
|
+ // 计算缩放后坐标
|
|
|
+ const postData = originalPoints.map((point: any) => {
|
|
|
+ const newX = (point[0] - pointB.x) * proportion.value + pointB.x;
|
|
|
+ const newY = (point[1] - pointB.y) * proportion.value + pointB.y;
|
|
|
+ return [newX, newY];
|
|
|
+ });
|
|
|
+ // console.log("原始坐标:", originalPoints);
|
|
|
+ // console.log("缩放后坐标:", postData);
|
|
|
+
|
|
|
+ direction.value = postData[0][0] - offset.x - (94 / 2);//鼻子X
|
|
|
+ //绘制头部
|
|
|
+ const point1 = { x: postData[4][0], y: postData[4][1] };
|
|
|
+ const point2 = { x: postData[3][0], y: postData[3][1] };
|
|
|
+ // 计算椭圆参数
|
|
|
+ const centerX = (point1.x + point2.x) / 2; // 椭圆中心X
|
|
|
+ const centerY = (point1.y + point2.y) / 2; // 椭圆中心Y
|
|
|
+ const distance = Math.sqrt(
|
|
|
+ Math.pow(point2.x - point1.x, 2) +
|
|
|
+ Math.pow(point2.y - point1.y, 2)
|
|
|
+ ); // 两个焦点之间的距离
|
|
|
+ const radiusX = distance * 0.5; // 水平半径(可调整)
|
|
|
+ const radiusY = distance * 0.6; // 垂直半径(可调整)
|
|
|
+ // 1. 绘制填充椭圆
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2);
|
|
|
+ ctx.fillStyle = 'red'; // 填充颜色
|
|
|
+ ctx.fill(); // 填充
|
|
|
+ // 2. 绘制边框
|
|
|
+ ctx.strokeStyle = 'red';
|
|
|
+ ctx.lineWidth = 5;
|
|
|
+ ctx.stroke();
|
|
|
+ // 绘制每个点
|
|
|
+ postData.forEach((point: any) => {
|
|
|
+ const [x, y] = point;
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.arc(x, y, 5, 0, Math.PI * 2); // 绘制半径为5的圆点
|
|
|
+ ctx.fillStyle = 'red';
|
|
|
+ ctx.fill();
|
|
|
+ ctx.lineWidth = 1;
|
|
|
+ ctx.stroke();
|
|
|
+ });
|
|
|
+ // 根据点关系连线
|
|
|
+ const arr = [[10, 8], [8, 6], [6, 5], [5, 7], [7, 9], [6, 12], [5, 11], [12, 11], [12, 14], [14, 16], [11, 13], [13, 15]]
|
|
|
+ arr.forEach((point: any) => {
|
|
|
+ let index1 = point[0];
|
|
|
+ let index2 = point[1];
|
|
|
+ //连线
|
|
|
+ const dian1 = { x: postData[index1][0], y: postData[index1][1] };
|
|
|
+ const dian2 = { x: postData[index2][0], y: postData[index2][1] };
|
|
|
+ // 绘制连线
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(dian1.x, dian1.y); // 起点
|
|
|
+ ctx.lineTo(dian2.x, dian2.y); // 终点
|
|
|
+ ctx.strokeStyle = 'red'; // 线条颜色
|
|
|
+ ctx.lineWidth = 3; // 线条宽度
|
|
|
+ ctx.stroke(); // 描边
|
|
|
+ });
|
|
|
+ ctx.restore(); // 恢复状态
|
|
|
+
|
|
|
+
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 计算B点的夹角度数(∠ABC)
|
|
|
+ * @param {Object} pointA - A点坐标 {x, y, z可选}
|
|
|
+ * @param {Object} pointB - B点坐标 {x, y, z可选}
|
|
|
+ * @param {Object} pointC - C点坐标 {x, y, z可选}
|
|
|
+ * @returns {number} B点的夹角度数(保留两位小数)
|
|
|
+ */
|
|
|
+function calculateAngleAtB(pointA, pointB, pointC) {
|
|
|
+ // 计算向量BA和向量BC
|
|
|
+ const vectorBA = {
|
|
|
+ x: pointA.x - pointB.x,
|
|
|
+ y: pointA.y - pointB.y,
|
|
|
+ z: (pointA.z || 0) - (pointB.z || 0)
|
|
|
+ };
|
|
|
+
|
|
|
+ const vectorBC = {
|
|
|
+ x: pointC.x - pointB.x,
|
|
|
+ y: pointC.y - pointB.y,
|
|
|
+ z: (pointC.z || 0) - (pointB.z || 0)
|
|
|
+ };
|
|
|
+
|
|
|
+ // 计算点积
|
|
|
+ const dotProduct =
|
|
|
+ vectorBA.x * vectorBC.x +
|
|
|
+ vectorBA.y * vectorBC.y +
|
|
|
+ vectorBA.z * vectorBC.z;
|
|
|
+
|
|
|
+ // 计算向量BA的模长
|
|
|
+ const lengthBA = Math.sqrt(
|
|
|
+ vectorBA.x ** 2 +
|
|
|
+ vectorBA.y ** 2 +
|
|
|
+ vectorBA.z ** 2
|
|
|
+ );
|
|
|
+
|
|
|
+ // 计算向量BC的模长
|
|
|
+ const lengthBC = Math.sqrt(
|
|
|
+ vectorBC.x ** 2 +
|
|
|
+ vectorBC.y ** 2 +
|
|
|
+ vectorBC.z ** 2
|
|
|
+ );
|
|
|
+
|
|
|
+ // 防止除以零的情况
|
|
|
+ if (lengthBA === 0 || lengthBC === 0) {
|
|
|
+ throw new Error("点A、B、C不能重合");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算余弦值
|
|
|
+ const cosine = dotProduct / (lengthBA * lengthBC);
|
|
|
+
|
|
|
+ // 由于计算误差可能导致cosine略超出[-1, 1]范围,需要修正
|
|
|
+ const clampedCosine = Math.max(Math.min(cosine, 1), -1);
|
|
|
+
|
|
|
+ // 计算弧度并转换为角度
|
|
|
+ const angleRadians = Math.acos(clampedCosine);
|
|
|
+ const angleDegrees = angleRadians * (180 / Math.PI);
|
|
|
+
|
|
|
+ // 保留两位小数并返回
|
|
|
+ return parseFloat(angleDegrees.toFixed(2));
|
|
|
+}
|
|
|
+
|
|
|
+onBeforeMount(() => {
|
|
|
+ clientObj.value = {
|
|
|
+ width: document.documentElement.clientWidth,
|
|
|
+ height: document.documentElement.clientHeight,
|
|
|
+ }
|
|
|
+ getInit();
|
|
|
+});
|
|
|
+
|
|
|
// 组件卸载时清理事件监听
|
|
|
onUnmounted(() => {
|
|
|
if (game.value && game.value.events && scoreListener) {
|