"""
手势打地鼠小游戏
---------------
打开摄像头后,用食指指尖隔空"戳"屏幕上随机冒出的地鼠即可得分。主要的核心是演示音效处理与交互处理,游戏逻辑不复杂
依赖: pip install pygame opencv-python mediapipe numpy
"""
import random
import time
import cv2
import mediapipe as mp
import numpy as np
import pygame
class FingerWhackGame:
"""基于手势识别的打地鼠游戏主类"""
def __init__(self):
# ---------- 游戏参数 ----------
# 3x3 地洞布局
self.holes = [(x, y) for y in (150, 350, 550) for x in (120, 340, 560)]
self.hole_r = 50 # 地洞半径
self.mole_r = 40 # 地鼠半径
self.aim_r = 14 # 指尖瞄准点半径
self.total_time = 45 # 单局时长(秒)
self.spawn_gap = 1.0 # 刷地鼠间隔(秒)
self.max_moles = 3 # 场上同时存在的地鼠上限
self.mole_lifetime = 2.0 # 地鼠停留时间(秒)
self.hit_show_time = 0.3 # 被打中后变色停留时间(秒)
self.fx_time = 0.5 # 命中特效持续时长(秒)
self.score_per_hit = 10 # 每打中一只的得分
# ---------- 音效 ----------
self._init_sound()
# ================= 音效相关 =================
def _init_sound(self):
"""初始化 pygame 音频,优先使用外部音效文件"""
pygame.mixer.init()
try:
self.whack_sfx = pygame.mixer.Sound("whack.wav")
except Exception:
print("[提示] 没有找到 whack.wav,改用程序合成的提示音")
self.whack_sfx = self._synthesize_beep()
@staticmethod
def _synthesize_beep():
"""用 numpy 现场合成一段短促的提示音,作为没有音效文件时的兜底"""
rate = 44100 # 采样率
length = 0.12 # 时长(秒)
freq = 660 # 频率(赫兹)
t = np.linspace(0, length, int(rate * length), endpoint=False)
tone = np.sin(2 * np.pi * freq * t)
tone *= np.exp(-6 * t) # 指数衰减,听起来更自然
pcm = (tone * 32767).astype(np.int16)
stereo = np.column_stack([pcm, pcm]) # 单声道复制成立体声
return pygame.mixer.Sound(buffer=stereo)
def _play_whack(self):
"""播放命中音效(失败时静默跳过,不影响游戏)"""
try:
self.whack_sfx.play()
except Exception as err:
print(f"[提示] 音效播放失败: {err}")
# ================= 摄像头 / 手势 =================
def init_camera(self):
"""打开默认摄像头,并加载 MediaPipe 手部关键点模型"""
self.cap = cv2.VideoCapture(0)
self.hand_detector = mp.solutions.hands.Hands(
static_image_mode=False, # 视频流模式
max_num_hands=1, # 只追踪一只手
min_detection_confidence=0.7,
min_tracking_confidence=0.5,
)
def locate_fingertip(self, frame):
"""识别画面中的手,返回食指指尖(8号关键点)的像素坐标"""
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
results = self.hand_detector.process(rgb)
if not results.multi_hand_landmarks:
return None
tip = results.multi_hand_landmarks[0].landmark[8]
h, w, _ = frame.shape
return int(tip.x * w), int(tip.y * h)
# ================= 画面绘制 =================
def render(self, frame, score, time_left, moles, fingertip, hit_fx=None):
"""把分数、倒计时、地洞、地鼠、指尖光标全部画到画面上"""
# 分数和剩余时间
cv2.putText(frame, f"Score: {score}", (40, 40),
cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
cv2.putText(frame, f"Time: {time_left}s", (500, 40),
cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
# 所有地洞
for pos in self.holes:
cv2.circle(frame, pos, self.hole_r, (100, 50, 0), -1)
# 地鼠:平时红色,被打中变绿
for mole in moles:
body_color = (0, 255, 0) if mole.get("hit") else (0, 0, 255)
mx, my = mole["pos"]
cv2.circle(frame, (mx, my), self.mole_r, body_color, -1)
# 两只眼睛
for dx in (-10, 10):
cv2.circle(frame, (mx + dx, my - 10), 5, (255, 255, 255), -1)
# 指尖瞄准点
if fingertip:
cv2.circle(frame, fingertip, self.aim_r, (0, 255, 0), -1)
# 命中特效:黄圈 + 飘分
if hit_fx:
fx_pos, _end_time = hit_fx
cv2.circle(frame, fx_pos, 60, (0, 255, 255), 5)
cv2.putText(frame, f"+{self.score_per_hit}",
(fx_pos[0] - 20, fx_pos[1] - 70),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2)
return frame
def _show_game_over(self, frame, score):
"""结算画面:显示 GAME OVER 和最终得分,停留 3 秒"""
cv2.putText(frame, "GAME OVER", (200, 300),
cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 255), 3)
cv2.putText(frame, f"Final Score: {score}", (200, 350),
cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
cv2.imshow("Finger Whack Game", frame)
cv2.waitKey(3000)
# ================= 主循环 =================
def run(self):
"""游戏主循环"""
self.init_camera()
score = 0
moles = [] # 元素: {"pos": (x, y), "born": 时刻, "hit": bool}
hit_fx = None # 命中特效: ((x, y), 结束时刻)
start_time = time.time()
last_spawn = 0.0
while True:
ok, frame = self.cap.read()
if not ok:
print("[错误] 无法读取摄像头画面")
break
frame = cv2.flip(frame, 1) # 水平镜像,交互更顺手
now = time.time()
time_left = max(0, self.total_time - int(now - start_time))
if time_left <= 0: # 时间到,结算后退出
self._show_game_over(frame, score)
break
# 指尖位置
tip = self.locate_fingertip(frame)
# 到点刷一只新地鼠(场上最多 max_moles 只,只挑空着的洞)
if now - last_spawn >= self.spawn_gap and len(moles) < self.max_moles:
occupied = {m["pos"] for m in moles}
free_holes = [p for p in self.holes if p not in occupied]
if free_holes:
moles.append({"pos": random.choice(free_holes),
"born": now, "hit": False})
last_spawn = now
# 清理离场地鼠:被打中的变色稍停一下,没被打中的到点消失
alive = []
for m in moles:
if m["hit"]:
if now - m["hit_at"] < self.hit_show_time:
alive.append(m)
elif now - m["born"] < self.mole_lifetime:
alive.append(m)
moles = alive
# 命中判定(判定半径 = 地鼠半径 + 瞄准点半径;平方距离省一次开方)
reach = self.mole_r + self.aim_r
if tip:
for m in moles:
if m["hit"]:
continue
dx = tip[0] - m["pos"][0]
dy = tip[1] - m["pos"][1]
if dx * dx + dy * dy <= reach * reach:
m["hit"] = True
m["hit_at"] = now
score += self.score_per_hit
hit_fx = (m["pos"], now + self.fx_time)
self._play_whack()
break # 一次只算打中一只
# 特效到期就关掉
if hit_fx and now > hit_fx[1]:
hit_fx = None
self.render(frame, score, time_left, moles, tip, hit_fx)
cv2.imshow("Finger Whack Game", frame)
key = cv2.waitKey(1) & 0xFF
if key == 27 or key == ord("q"): # ESC 或 Q 退出
break
# 释放资源
self.cap.release()
cv2.destroyAllWindows()
pygame.mixer.quit()
print(f"游戏结束! 你的最终得分: {score}")
if __name__ == "__main__":
game = FingerWhackGame()
game.run()