【基于Python的左右手贪吃蛇游戏】

1、游戏概述

这是一个创新的双人贪吃蛇游戏,使用计算机视觉技术通过摄像头捕捉玩家的左右手动作来控制两条独立的蛇。左手控制红色蛇,右手控制蓝色蛇,在同一个竞技场中竞相吃食物,增加长度并争取最高分数。

源码在文末

2、游戏特色

  1. 双人或双手实时对战:左右手分别控制两条蛇,实现真正的同屏竞技

  2. 手势控制:无需键盘或鼠标,直接通过手部动作控制游戏

  3. 智能碰撞检测:精确的自我碰撞检测机制

  4. 中文界面:完整的中文提示和分数显示

  5. 可调节灵敏度:实时调整碰撞检测阈值

3、技术实现

3.1、核心依赖库

python

import math
import random
import cvzone
import cv2
import numpy as np
from cvzone.HandTrackingModule import HandDetector
from PIL import Image, ImageDraw, ImageFont

3.2、游戏核心类:DualSnakeGameClass

初始化设置

游戏初始化时创建两条独立的蛇,分别具有不同的颜色和属性:

python

self.snakes = {
    "left": {
        "points": [],           # 蛇身点坐标
        "lengths": [],          # 各段长度
        "currentLength": 0,     # 当前总长度
        "allowedLength": 150,   # 允许的最大长度
        "previousHead": (0, 0), # 前一帧头部位置
        "score": 0,             # 得分
        "color": (0, 0, 255),   # 蛇身颜色(红色)
        "head_color": (200, 0, 200)  # 蛇头颜色(紫色)
    },
    "right": {
        # ... 类似配置,颜色为蓝色和青色
    }
}
手势检测与蛇的移动

游戏使用MediaPipe手部关键点检测技术,通过食指指尖的位置来控制蛇的移动方向:

python

# 检测手部关键点
hands, img = detector.findHands(img, flipType=False, draw=True)

# 获取左右手食指位置
for hand in hands:
    lmList = hand['lmList']
    pointIndex = lmList[8][0:2]  # 食指指尖坐标
    
    if hand['type'] == 'Left':
        left_hand_head = pointIndex
    else:  # Right
        right_hand_head = pointIndex
蛇身更新算法

采用动态长度控制机制,确保蛇身平滑移动:

python

def update_snake(self, snake_data, currentHead):
    px, py = snake_data["previousHead"]
    cx, cy = currentHead
    
    # 添加新点并计算距离
    snake_data["points"].append([cx, cy])
    distance = math.hypot(cx - px, cy - py)
    snake_data["lengths"].append(distance)
    snake_data["currentLength"] += distance
    
    # 长度控制
    if snake_data["currentLength"] > snake_data["allowedLength"]:
        while snake_data["currentLength"] > snake_data["allowedLength"]:
            snake_data["currentLength"] -= snake_data["lengths"].pop(0)
            snake_data["points"].pop(0)
碰撞检测系统

实现精确的自我碰撞检测,避免误判:

python

def check_collision(self, snake_data):
    if len(snake_data["points"]) > 30 and self.collisionCooldown == 0:
        # 排除近期的点,防止误检测
        check_points = snake_data["points"][:-20]
        
        if len(check_points) > 10:
            cx, cy = snake_data["points"][-1]  # 蛇头位置
            
            # 计算蛇头与蛇身各点的最小距离
            body_points = np.array(check_points, dtype=np.float32)
            head_point = np.array([[cx, cy]], dtype=np.float32)
            distances = np.sqrt(np.sum((body_points - head_point) ** 2, axis=1))
            min_distance = np.min(distances)
            
            # 如果最小距离小于阈值,判定为碰撞
            if min_distance < self.min_collision_distance:
                return True
    return False
中文文本渲染

使用PIL库实现中文字符的渲染:

python

def put_chinese_text(self, img, text, position, font_size=30, color=(0, 255, 0)):
    img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    draw = ImageDraw.Draw(img_pil)
    try:
        font = ImageFont.truetype("simhei.ttf", font_size)
    except:
        font = ImageFont.truetype("msyh.ttc", font_size)
    draw.text(position, text, font=font, fill=color)
    return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)

4、游戏玩法

4.1、游戏状态

  1. 等待开始:显示操作说明,等待玩家准备

  2. 游戏进行:实时显示双蛇移动和分数

  3. 游戏结束:显示最终分数和获胜方

4.2、控制方式

  • 手势控制:左右手食指控制对应颜色的蛇

  • 键盘控制

    • 空格键:开始游戏/重新开始

    • Q键:退出游戏

    • +/-键:调整碰撞检测灵敏度

5、运行配置

5.1、环境要求

确保PC或笔记本摄像头打开以确保运行成功

运行软件:本人使用PyCharm

  • Python 3.9+

其余库:

  • OpenCV

  • cvzone

  • MediaPipe

  • NumPy

  • Pillow

5.2、安装依赖

bash

python -m pip install mediapipe

5.3、启动方式

  1. 确保摄像头连接正常

  2. 运行Python脚本:

6、游戏策略与技巧

  1. 控制精度:保持手部稳定移动,避免剧烈晃动

  2. 路径规划:预判食物位置,规划最优路径

  3. 风险规避:避免蛇身缠绕过紧,留出安全空间

  4. 灵敏度调整:根据个人控制习惯调整碰撞阈值

7、源码如下

运行效果展示:https://www.douyin.com/user/self?modal_id=7568376642662305039

import math
import random
import cvzone
import cv2
import numpy as np
from cvzone.HandTrackingModule import HandDetector
from PIL import Image, ImageDraw, ImageFont

cap = cv2.VideoCapture(0)
cap.set(3, 1280)
cap.set(4, 720)

detector = HandDetector(detectionCon=0.7, maxHands=2)


class DualSnakeGameClass:
    def __init__(self, pathFood):
        # 两条蛇的数据
        self.snakes = {
            "left": {
                "points": [],
                "lengths": [],
                "currentLength": 0,
                "allowedLength": 150,
                "previousHead": (0, 0),
                "score": 0,
                "color": (0, 0, 255),  # 红色
                "head_color": (200, 0, 200)  # 紫色
            },
            "right": {
                "points": [],
                "lengths": [],
                "currentLength": 0,
                "allowedLength": 150,
                "previousHead": (0, 0),
                "score": 0,
                "color": (255, 0, 0),  # 蓝色
                "head_color": (0, 200, 200)  # 青色
            }
        }

        # 食物
        self.imgFood = cv2.imread(pathFood, cv2.IMREAD_UNCHANGED)
        if self.imgFood is None:
            print(f"错误:无法从 {pathFood} 加载图像")
            self.imgFood = np.ones((50, 50, 4), dtype=np.uint8) * 255
            cv2.circle(self.imgFood, (25, 25), 20, (0, 255, 0, 255), -1)

        self.hFood, self.wFood, _ = self.imgFood.shape
        self.foodPoint = 0, 0
        self.randomFoodLocation()

        # 游戏状态
        self.gameStarted = False
        self.gameOver = False
        self.collisionCooldown = 0
        self.min_collision_distance = 45

    def randomFoodLocation(self):
        self.foodPoint = random.randint(100, 1000), random.randint(100, 600)

    def put_chinese_text(self, img, text, position, font_size=30, color=(0, 255, 0)):
        """使用PIL在图像上绘制中文文本"""
        img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
        draw = ImageDraw.Draw(img_pil)
        try:
            font = ImageFont.truetype("simhei.ttf", font_size)
        except:
            try:
                font = ImageFont.truetype("msyh.ttc", font_size)
            except:
                font = ImageFont.load_default()
                print("警告:未找到中文字体,中文可能显示异常")
        draw.text(position, text, font=font, fill=color)
        return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)

    def update_snake(self, snake_data, currentHead):
        """更新单条蛇的状态"""
        px, py = snake_data["previousHead"]
        cx, cy = currentHead

        snake_data["points"].append([cx, cy])
        distance = math.hypot(cx - px, cy - py)
        snake_data["lengths"].append(distance)
        snake_data["currentLength"] += distance
        snake_data["previousHead"] = cx, cy

        # 长度控制
        if snake_data["currentLength"] > snake_data["allowedLength"]:
            while snake_data["currentLength"] > snake_data["allowedLength"] and snake_data["lengths"]:
                snake_data["currentLength"] -= snake_data["lengths"].pop(0)
                snake_data["points"].pop(0)

        # 检查是否吃到食物
        rx, ry = self.foodPoint
        if rx - self.wFood // 2 < cx < rx + self.wFood // 2 and \
                ry - self.hFood // 2 < cy < ry + self.hFood // 2:
            self.randomFoodLocation()
            snake_data["allowedLength"] += 50
            snake_data["score"] += 1
            print(f"{'左手' if snake_data['color'] == (0, 0, 255) else '右手'}得分! 当前分数: {snake_data['score']}")

    def draw_snake(self, img, snake_data):
        """绘制单条蛇"""
        if len(snake_data["points"]) > 1:
            points_array = np.array(snake_data["points"], dtype=np.int32)
            for i in range(1, len(points_array)):
                cv2.line(img,
                         tuple(points_array[i - 1]),
                         tuple(points_array[i]),
                         snake_data["color"], 20)
            # 画蛇头
            cv2.circle(img, tuple(points_array[-1]), 20, snake_data["head_color"], cv2.FILLED)

    def check_collision(self, snake_data):
        """检查单条蛇是否碰撞到自己"""
        if len(snake_data["points"]) > 30 and self.collisionCooldown == 0:  # 修改这里
            check_points = snake_data["points"][:-20]  # 修改这里,排除更多点
            if len(check_points) > 10:  # 修改这里
                cx, cy = snake_data["points"][-1]  # 蛇头位置
                pts = np.array(check_points, dtype=np.int32)
                pts = pts.reshape((-1, 1, 2))

                head_point = np.array([[cx, cy]], dtype=np.float32)
                body_points = np.array(check_points, dtype=np.float32)
                distances = np.sqrt(np.sum((body_points - head_point) ** 2, axis=1))
                min_distance = np.min(distances)

                # 添加调试信息
                snake_name = "左手" if snake_data["color"] == (0, 0, 255) else "右手"
                print(f"{snake_name}蛇 - 点数: {len(snake_data['points'])}, 最小距离: {min_distance:.1f}")

                if min_distance < self.min_collision_distance:
                    print(f"{snake_name}蛇碰撞! 距离: {min_distance:.1f}")
                    return True
        return False

    def update(self, imgMain, left_hand_head, right_hand_head):
        if not self.gameStarted:
            # 等待开始界面
            imgMain = self.put_chinese_text(imgMain, "左右手贪吃蛇", (500, 10), font_size=60, color=(0, 255, 0))
            imgMain = self.put_chinese_text(imgMain, "请确保左右手都在摄像头范围内", (540, 70), font_size=20,
                                            color=(0, 255, 0))
            imgMain = self.put_chinese_text(imgMain, "按空格键开始游戏", (470, 800), font_size=30, color=(0, 255, 0))

            # 显示等待检测的手
            if left_hand_head != (0, 0):
                cv2.circle(imgMain, left_hand_head, 20, (0, 0, 255), cv2.FILLED)
                imgMain = self.put_chinese_text(imgMain, "左手已就绪", (left_hand_head[0] + 30, left_hand_head[1]),
                                                font_size=20, color=(0, 255, 0))
            if right_hand_head != (0, 0):
                cv2.circle(imgMain, right_hand_head, 20, (255, 0, 0), cv2.FILLED)
                imgMain = self.put_chinese_text(imgMain, "右手已就绪", (right_hand_head[0] + 30, right_hand_head[1]),
                                                font_size=20, color=(0, 255, 0))

        elif self.gameOver:
            # 游戏结束界面
            left_score = self.snakes["left"]["score"]
            right_score = self.snakes["right"]["score"]
            winner = "左手" if left_score > right_score else "右手" if right_score > left_score else "平手"

            imgMain = self.put_chinese_text(imgMain, "GameOver", (480, 300), font_size=60, color=(0, 255, 0))
            imgMain = self.put_chinese_text(imgMain, f'左手得分: {left_score}', (20, 550), font_size=40,
                                            color=(0, 255, 0))
            imgMain = self.put_chinese_text(imgMain, f'右手得分: {right_score}', (1000, 550), font_size=40,
                                            color=(0, 255, 0))
            imgMain = self.put_chinese_text(imgMain, f'获胜手: {winner}', (470, 520), font_size=40, color=(0, 255, 0))
            imgMain = self.put_chinese_text(imgMain, "按空格键重新开始", (470, 600), font_size=30, color=(0, 255, 0))
        else:
            # 游戏进行中
            # 更新两条蛇
            if left_hand_head != (0, 0):
                self.update_snake(self.snakes["left"], left_hand_head)
            if right_hand_head != (0, 0):
                self.update_snake(self.snakes["right"], right_hand_head)

            # 绘制两条蛇
            self.draw_snake(imgMain, self.snakes["left"])
            self.draw_snake(imgMain, self.snakes["right"])

            # 绘制食物
            rx, ry = self.foodPoint
            imgMain = cvzone.overlayPNG(imgMain, self.imgFood,
                                        (rx - self.wFood // 2, ry - self.hFood // 2))

            # 显示分数
            left_score = self.snakes["left"]["score"]
            right_score = self.snakes["right"]["score"]
            imgMain = self.put_chinese_text(imgMain, f'左手: {left_score}分', (50, 80), font_size=30, color=(0, 255, 0))
            imgMain = self.put_chinese_text(imgMain, f'右手: {right_score}分', (1000, 80), font_size=30,
                                            color=(0, 255, 0))

            # 碰撞冷却
            if self.collisionCooldown > 0:
                self.collisionCooldown -= 1

            # 只有当蛇有一定长度时才检测碰撞
            left_collision = len(self.snakes["left"]["points"]) > 30 and self.check_collision(self.snakes["left"])
            right_collision = len(self.snakes["right"]["points"]) > 30 and self.check_collision(self.snakes["right"])

            if left_collision or right_collision:
                self.gameOver = True
                self.collisionCooldown = 30

        return imgMain

    def reset(self):
        """重置游戏状态"""
        for snake in self.snakes.values():
            snake["points"] = []
            snake["lengths"] = []
            snake["currentLength"] = 0
            snake["allowedLength"] = 150
            snake["previousHead"] = (0, 0)
            snake["score"] = 0

        self.randomFoodLocation()
        self.gameStarted = False
        self.gameOver = False
        self.collisionCooldown = 0
        print("游戏已重置")

    def start_game(self):
        """开始游戏"""
        self.gameStarted = True
        self.gameOver = False
        print("游戏开始!")


# 创建游戏实例
game = DualSnakeGameClass("xin.png")

while True:
    success, img = cap.read()
    if not success:
        print("无法从摄像头读取")
        break

    img = cv2.flip(img, 1)
    hands, img = detector.findHands(img, flipType=False, draw=True)

    # 初始化手部位置
    left_hand_head = (0, 0)
    right_hand_head = (0, 0)

    # 检测左右手
    if hands:
        for hand in hands:
            lmList = hand['lmList']
            pointIndex = lmList[8][0:2]  # 食指指尖

            if hand['type'] == 'Left':
                left_hand_head = pointIndex
                cv2.circle(img, pointIndex, 15, (0, 0, 255), cv2.FILLED)  # 红色圆点表示左手
            else:  # Right
                right_hand_head = pointIndex
                cv2.circle(img, pointIndex, 15, (255, 0, 0), cv2.FILLED)  # 蓝色圆点表示右手

    # 更新游戏状态
    img = game.update(img, left_hand_head, right_hand_head)

    # 显示操作说明
    if not game.gameStarted:
        img = game.put_chinese_text(img, "等待开始游戏...", (50, 600), font_size=20, color=(0, 255, 0))
    elif not game.gameOver:
        img = game.put_chinese_text(img, "游戏进行中", (50, 600), font_size=20, color=(0, 255, 0))

    img = game.put_chinese_text(img, "空格键:开始/重新开始  Q:退出  +/-:灵敏度", (50, 630), font_size=18,
                                color=(0, 255, 0))

    cv2.imshow("Left and right hand snake game", img)
    key = cv2.waitKey(1)

    if key == ord(' '):  # 空格键
        if not game.gameStarted:
            game.start_game()
        else:
            game.reset()
    elif key == ord('q'):
        break
    elif key == ord('+') or key == ord('='):
        game.min_collision_distance += 5
        print(f"碰撞阈值增加到: {game.min_collision_distance}")
    elif key == ord('-') or key == ord('_'):
        if game.min_collision_distance > 20:
            game.min_collision_distance -= 5
            print(f"碰撞阈值减少到: {game.min_collision_distance}")

cap.release()
cv2.destroyAllWindows()

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值