1、游戏概述
这是一个创新的双人贪吃蛇游戏,使用计算机视觉技术通过摄像头捕捉玩家的左右手动作来控制两条独立的蛇。左手控制红色蛇,右手控制蓝色蛇,在同一个竞技场中竞相吃食物,增加长度并争取最高分数。
源码在文末
2、游戏特色
-
双人或双手实时对战:左右手分别控制两条蛇,实现真正的同屏竞技
-
手势控制:无需键盘或鼠标,直接通过手部动作控制游戏
-
智能碰撞检测:精确的自我碰撞检测机制
-
中文界面:完整的中文提示和分数显示
-
可调节灵敏度:实时调整碰撞检测阈值
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、游戏状态
-
等待开始:显示操作说明,等待玩家准备
-
游戏进行:实时显示双蛇移动和分数
-
游戏结束:显示最终分数和获胜方
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、启动方式
-
确保摄像头连接正常
-
运行Python脚本:
6、游戏策略与技巧
-
控制精度:保持手部稳定移动,避免剧烈晃动
-
路径规划:预判食物位置,规划最优路径
-
风险规避:避免蛇身缠绕过紧,留出安全空间
-
灵敏度调整:根据个人控制习惯调整碰撞阈值
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()
1846

被折叠的 条评论
为什么被折叠?



