隔空操作,手势识别其础练习,一个简单的小游戏,通过这个游戏来练习,手势操作与物体交互,加上声效

"""
手势打地鼠小游戏
---------------
打开摄像头后,用食指指尖隔空"戳"屏幕上随机冒出的地鼠即可得分。主要的核心是演示音效处理与交互处理,游戏逻辑不复杂
依赖: 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()
posted @ 2026-08-25 12:59  鬼门元歌  阅读(21)  评论(0)    收藏  举报