效果
运行环境
Anaconda 22.9.0
Visual Studio Code
Python 3.9.12
PyQT 5.2
具体过程
准备
1.创建一个文件夹,随便起个名,代表这个游戏的名字
2.在这个文件夹里面再创建一个文件夹,可以将素材放入新建的文件夹
3.创建一个py文件,就可以开始了
4.我们优先做游戏主体部分,然后再慢慢完善
1.导入需要的库
import cv2
import time
import cvzone
from cvzone.HandTrackingModule import HandDetector
from winsound import Beep
import numpy as np
import random
2.打开摄像头,设置页面大小
cap = cv2.VideoCapture(0)
cap.set(3, 1280)
cap.set(4, 720)
3.读取素材
imgBackground = cv2.imread("picture\Background.png")#背景板
imgBall = cv2.imread("picture\Ball.png", cv2.IMREAD_UNCHANGED)#小球
imgBat1 = cv2.imread("picture\Bat1.png", cv2.IMREAD_UNCHANGED)#球拍
imgBat2 = cv2.imread("picture\Bat2.png", cv2.IMREAD_UNCHANGED)#球拍
4.初始化数据
1.初始化小球和成绩
# 小球随机方向
speed = [-20,20]
# 小球的初始位置在球桌中央
ballPos = [600, 320]
# 小球随机初始速度
speedX = random.choice(speed)
speedY = random.choice(speed)
# 设置游戏为开始
gameOver = False
# 初始分数
score = [0, 0]
2.导入手部识别
detector = HandDetector(detectionCon=0.8, maxHands=2)
hands, img = detector.findHands(img)
img = cv2.flip(img, 1)
hands, img = detector.findHands(img, flipType=False)
5.逻辑实现
5.1.双手模式
# 如果手在镜头内
if hands:
for hand in hands:
x, y, w, h = hand['bbox']
# 返回球拍的高度,宽度
h1, w1 = imgBat1.shape[0:2]
y1 = y - h1 // 2
# 规定球拍的上下范围
y1 = np.clip(y1, 20, 520)
# 如果检测为左手
if hand['type'] == "Left":
# 将板放在背景上,横坐标不动,纵坐标根据手的位置来上下移动
img = cvzone.overlayPNG(img, imgBat1, (60, y1))
# 如果球碰到了板
if 70 < ballPos[0] < 70 + w1 and y1-h1//3 < ballPos[1] < y1 + h1:
# 将横向速度反转,也就是反弹
speedX = -speedX
ballPos[0] += 30
score[0] += 1
# 撞击声
Beep(1046,50)
#如果检测为右手
if hand['type'] == "Right":
img = cvzone.overlayPNG(img, imgBat2, (1190, y1))
if 1140 - w1 < ballPos[0] < 1140 and y1-h1//3 < ballPos[1] < y1 + h1:
speedX = -speedX
ballPos[0] -= 30
score[1] += 1
Beep(1046,50)
if ballPos[0] < 40 or ballPos[0] > 1160:
gameOver = True
实时成绩显示
cv2.putText(img,str(score[0]),(300,650),cv2.FONT_HERSHEY_COMPLEX,3,(255,255,255),5)
cv2.putText(img,str(score[1]),(900,650),cv2.FONT_HERSHEY_COMPLEX,3,(255,255, 255),5)
结束界面显示
# 左边分数高
if score[0]>score[1]:
cv2.putText(img, str('WIN'), (250, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (255,0,255), 5)
cv2.putText(img, str('LOSE'), (850, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (34, 148, 83), 5)
# 右边分数高
elif score[1]>score[0]:
cv2.putText(img, str('LOSE'), (250, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (34, 148, 83), 5)
cv2.putText(img, str('WIN'), (850, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (255,0,255), 5)
# 平局显示
elif score[0]==score[1]:
cv2.putText(img, str('DRAW'), (500, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (192, 44, 56), 5)
5.2.单手模式
if hands:
for hand in hands:
x, y, w, h = hand['bbox']
h1, w1 = imgBat1.shape[0:2]
y1 = y - h1 // 2
y1 = np.clip(y1, 20, 520)
if hand['type'] == "Left":
img = cvzone.overlayPNG(img, imgBat1, (60, y1))
if 70 < ballPos[0] < 70 + w1 and y1-h1//3 < ballPos[1] < y1 + h1:
speedX = -speedX
ballPos[0] += 30
score[0] += 1
Beep(1046,50)
# 如果球超过板
if ballPos[0] < 40 :
# 游戏结束
gameOver = True
# 如果球碰到右边的墙
if ballPos[0] >= 1200 :
speedX = -speedX
# 显示实时成绩
cv2.putText(img,str(score[0]),(300,650),cv2.FONT_HERSHEY_COMPLEX,3,(255, 255, 255), 5)
if hand['type'] == "Right":
img = cvzone.overlayPNG(img, imgBat2, (1190, y1))
if 1140 - w1 < ballPos[0] < 1140 and y1-h1//3 < ballPos[1] < y1 + h1:
speedX = -speedX
ballPos[0] -= 30
score[1] += 1
Beep(1046,50)
# 如果球超过板
if ballPos[0] > 1160 :
# 游戏结束
gameOver = True
# 如果球碰到左边的墙
if ballPos[0] <= 20 :
# 反弹
speedX = -speedX
cv2.putText(img,str(score[1]),(900,650),cv2.FONT_HERSHEY_COMPLEX,3,(255, 255, 255), 5)
# 如果没有检测到手并且球超出左右边界(无缝衔接左右手的核心)
if not hands and (ballPos[0] < 40 or ballPos[0] > 1160):
# 游戏结束
gameOver = True
5.3游戏结束界面
if gameOver:
imgGameOver = cv2.imread("picture\Gameover.png")
img = imgGameOver
cvzone.putTextRect(img, f"'R' begin or 'ESC' end", (400,100))
5.4帧数画面
pTime = 0
cTime = 0
将计算过程与显示写进while循环里
cTime = time.time()
fps = 1/(cTime-pTime)
pTime = cTime
cv2.putText(img,f"FPS:{int(fps)}",(20,70),cv2.FONT_HERSHEY_PLAIN,3,(255, 255, 0),3)
5.5移动小球
ballPos[0] += speedX
ballPos[1] += speedY
# 显示小球轨迹(背景,小球,坐标)
img = cvzone.overlayPNG(img, imgBall, ballPos)
5.6重新开始或者退出游戏
if key == ord('R'):
# 恢复中央位置
ballPos = [600, 320]
# 小球恢复随机初始速度
speedX = random.choice(speed)
speedY = random.choice(speed)
# 关闭游戏结束画面
gameOver = False
# 恢复初始化成绩
score = [0, 0]
elif key == 27:
# 退出
break
完整代码
UI.py
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from UI_Set import Ui_Game
import game
class MyMainForm(QMainWindow,Ui_Game):
def __init__(self, parent=None):
super(MyMainForm, self).__init__(parent)
self.setupUi(self)
self.Button1.clicked.connect(game.One_hand)
self.Button2.clicked.connect(game.Both_hands)
self.setWindowTitle("趣味乒乓球")
if __name__ == "__main__":
app = QApplication(sys.argv)
mygame = MyMainForm()
mygame.setObjectName("MainWindow")
mygame.show()
sys.exit(app.exec_())
game.py
import cv2
import time
import cvzone
from cvzone.HandTrackingModule import HandDetector
from winsound import Beep
import numpy as np
import random
imgBackground = cv2.imread("picture\Background.png")
imgBall = cv2.imread("picture\Ball.png", cv2.IMREAD_UNCHANGED)
imgBat1 = cv2.imread("picture\Bat1.png", cv2.IMREAD_UNCHANGED)
imgBat2 = cv2.imread("picture\Bat2.png", cv2.IMREAD_UNCHANGED)
cap = cv2.VideoCapture(0)
cap.set(3, 1280)
cap.set(4, 720)
def Both_hands(self):
detector = HandDetector(detectionCon=0.8, maxHands=2)
pTime = 0
cTime = 0
speed = [-20,20]
ballPos = [600, 320]
speedX = random.choice(speed)
speedY = random.choice(speed)
gameOver = False
score = [0, 0]
while True:
imgGameOver = cv2.imread("picture\Gameover.png")
_, img = cap.read()
img = cv2.flip(img, 1)
cTime = time.time()
fps = 1/(cTime-pTime)
pTime = cTime
cv2.putText(img,f"FPS:{int(fps)}",(20,70),cv2.FONT_HERSHEY_PLAIN,3,(255, 255, 0),3)
hands, img = detector.findHands(img, flipType=False)
img = cv2.addWeighted(img, 0.2, imgBackground, 0.8, 0)
if hands:
for hand in hands:
x, y, w, h = hand['bbox']
h1, w1 = imgBat1.shape[0:2]
y1 = y - h1 // 2
y1 = np.clip(y1, 20, 520)
if hand['type'] == "Left":
img = cvzone.overlayPNG(img, imgBat1, (60, y1))
if 70 < ballPos[0] < 70 + w1 and y1-h1//3 < ballPos[1] < y1 + h1:
speedX = -speedX
ballPos[0] += 30
score[0] += 1
Beep(1046,50)
if hand['type'] == "Right":
img = cvzone.overlayPNG(img, imgBat2, (1190, y1))
if 1140 - w1 < ballPos[0] < 1140 and y1-h1//3 < ballPos[1] < y1 + h1:
speedX = -speedX
ballPos[0] -= 30
score[1] += 1
Beep(1046,50)
if ballPos[0] < 40 or ballPos[0] > 1160:
gameOver = True
if gameOver:
img = imgGameOver
cvzone.putTextRect(img, f"'R' begin or 'ESC' end", (400,100))
if score[0]>score[1]:
cv2.putText(img, str('WIN'), (250, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (255,0,255), 5)
cv2.putText(img, str('LOSE'), (850, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (34, 148, 83), 5)
elif score[1]>score[0]:
cv2.putText(img, str('LOSE'), (250, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (34, 148, 83), 5)
cv2.putText(img, str('WIN'), (850, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (255,0,255), 5)
elif score[0]==score[1]:
cv2.putText(img, str('DRAW'), (500, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (192, 44, 56), 5)
else:
if ballPos[1] >= 600 or ballPos[1] <= 10:
speedY = -speedY
ballPos[0] += speedX
ballPos[1] += speedY
img = cvzone.overlayPNG(img, imgBall, ballPos)
cv2.putText(img, str(score[0]), (300, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (255, 255, 255), 5)
cv2.putText(img, str(score[1]), (900, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (255, 255, 255), 5)
cv2.imshow("Both_hands", img)
key = cv2.waitKey(1)
if key == ord('R'):
ballPos = [600, 320]
speedX = random.choice(speed)
speedY = random.choice(speed)
gameOver = False
score = [0, 0]
elif key == 27:
break
cv2.destroyAllWindows()
def One_hand(self):
detector = HandDetector(detectionCon=0.8, maxHands=1)
pTime = 0
cTime = 0
speed = [-15,15]
ballPos = [600, 320]
speedX = random.choice(speed)
speedY = random.choice(speed)
gameOver = False
score = [0, 0]
while True:
imgGameOver = cv2.imread("picture\Gameover.png")
_, img = cap.read()
img = cv2.flip(img, 1)
cTime = time.time()
fps = 1/(cTime-pTime)
pTime = cTime
cv2.putText(img,f"FPS:{int(fps)}",(20,70),cv2.FONT_HERSHEY_PLAIN,3,(255, 255, 0),3)
hands, img = detector.findHands(img, flipType=False)
img = cv2.addWeighted(img, 0.2, imgBackground, 0.8, 0)
if hands:
for hand in hands:
x, y, w, h = hand['bbox']
h1, w1 = imgBat1.shape[0:2]
y1 = y - h1 // 2
y1 = np.clip(y1, 20, 520)
if hand['type'] == "Left":
img = cvzone.overlayPNG(img, imgBat1, (60, y1))
if 70 < ballPos[0] < 70 + w1 and y1-h1//3 < ballPos[1] < y1 + h1:
speedX = -speedX
ballPos[0] += 30
score[0] += 1
Beep(1046,50)
if ballPos[0] < 40 :
gameOver = True
if ballPos[0] >= 1200 :
speedX = -speedX
cv2.putText(img, str(score[0]), (300, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (255, 255, 255), 5)
if hand['type'] == "Right":
img = cvzone.overlayPNG(img, imgBat2, (1190, y1))
if 1140 - w1 < ballPos[0] < 1140 and y1-h1//3 < ballPos[1] < y1 + h1:
speedX = -speedX
ballPos[0] -= 30
score[1] += 1
Beep(1046,50)
if ballPos[0] > 1160 :
gameOver = True
if ballPos[0] <= 20 :
speedX = -speedX
cv2.putText(img, str(score[1]), (900, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (255, 255, 255), 5)
if not hands and (ballPos[0] < 40 or ballPos[0] > 1160):
gameOver = True
if gameOver:
img = imgGameOver
cvzone.putTextRect(img, f"'R' begin or 'ESC' end", (400,100))
else:
if ballPos[1] >= 600 or ballPos[1] <= 10:
speedY = -speedY
ballPos[0] += speedX
ballPos[1] += speedY
img = cvzone.overlayPNG(img, imgBall, ballPos)
cv2.imshow("One_hand", img)
key = cv2.waitKey(1)
if key == ord('R'):
ballPos = [600, 320]
speedX = random.choice(speed)
speedY = random.choice(speed)
gameOver = False
score = [0, 0]
elif key == 27:
break
cv2.destroyAllWindows()
UI_Set.py
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'e:\my_opencv\pingpong\pingpong5.0\First.ui'
#
# Created by: PyQt5 UI code generator 5.15.9
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Game(object):
def setupUi(self, Game):
Game.setObjectName("Game")
Game.setEnabled(True)
Game.resize(854, 716)
Game.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
self.textEdit = QtWidgets.QTextEdit(Game)
self.textEdit.setGeometry(QtCore.QRect(11, 11, 831, 511))
self.textEdit.viewport().setProperty("cursor", QtGui.QCursor(QtCore.Qt.ArrowCursor))
self.textEdit.setStyleSheet("background-color: rgb(255, 255, 255,60)")
self.textEdit.setObjectName("textEdit")
self.Button1 = QtWidgets.QPushButton(Game)
self.Button1.setGeometry(QtCore.QRect(20, 550, 231, 131))
self.Button1.setCursor(QtGui.QCursor(QtCore.Qt.OpenHandCursor))
self.Button1.setStyleSheet("background-color: rgb(255, 255, 255,60)")
self.Button1.setObjectName("Button1")
self.Button2 = QtWidgets.QPushButton(Game)
self.Button2.setGeometry(QtCore.QRect(590, 550, 241, 131))
self.Button2.setCursor(QtGui.QCursor(QtCore.Qt.OpenHandCursor))
self.Button2.setStyleSheet("background-color: rgb(255, 255, 255,60)")
self.Button2.setObjectName("Button2")
self.retranslateUi(Game)
QtCore.QMetaObject.connectSlotsByName(Game)
def retranslateUi(self, Game):
_translate = QtCore.QCoreApplication.translate
Game.setWindowTitle(_translate("Game", "Form"))
self.textEdit.setHtml(_translate("Game", "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
"<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">\n"
"p, li { white-space: pre-wrap; }\n"
"</style></head><body style=\" font-family:\'SimSun\'; font-size:9pt; font-weight:400; font-style:normal;\">\n"
"<p align=\"center\" style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"><br /></p>\n"
"<p align=\"center\" style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"><br /></p>\n"
"<p align=\"center\" style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:48pt; color:#ff0000;\"><br /></p>\n"
"<p align=\"center\" style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"><span style=\" font-size:48pt; color:#ff0000;\">趣味乒乓球</span></p>\n"
"<p align=\"center\" style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"><span style=\" font-size:18pt; color:#0000ff;\">炒鸡好玩的手势解压小游戏,简单上手</span></p>\n"
"<p align=\"center\" style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"><span style=\" font-size:20pt; color:#000000;\">游戏规则</span><span style=\" font-size:20pt; color:#550000;\">:</span><span style=\" font-size:16pt; color:#550000;\">球拍击打球得分,球碰到上下会反弹,球出界游戏结束,按</span><span style=\" font-size:16pt; color:#00ff00;\">ESC退出</span><span style=\" font-size:16pt; color:#550000;\">游戏或者按</span><span style=\" font-size:16pt; color:#ff5500;\">R重新开始</span><span style=\" font-size:16pt; color:#550000;\">游戏</span></p>\n"
"<p align=\"center\" style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:16pt; color:#550000;\"><br /></p>\n"
"<p align=\"center\" style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"><span style=\" font-size:20pt; color:#000000;\"> 单手模式:</span><span style=\" font-size:16pt; color:#000000;\">出现一侧球拍时,对面的边界可以让球反弹</span></p>\n"
"<p align=\"center\" style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"><span style=\" font-size:20pt; color:#000000;\"> 双手模式:</span><span style=\" font-size:16pt; color:#000000;\">只有上下会反弹,左边右边球拍都需要控制</span></p>\n"
"<p style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:24pt; color:#550000;\"><br /></p></body></html>"))
self.Button1.setText(_translate("Game", "单手模式"))
self.Button2.setText(_translate("Game", "双手模式"))
素材
结尾
第一次写博客,说实话,我挺喜欢这个游戏的。第一版的时候经常出错,球拍拍不到球之类的,到后面出的单手模式,也是蛮好玩的。喜欢的可以点个收藏关注,有什么不对的地方请指出。
参考
https://blog.csdn.net/weixin_42506516/article/details/125393153