import datetime
import cv2
import numpy as np
import random
from collections import deque
import mediapipe as mp
import json
import os
import platform
import time
import threading
from queue import Queue
from PIL import Image, ImageDraw, ImageFont
import pygame.mixer
# ------------------------------
# 全局队列与事件
# ------------------------------
frame_queue = Queue(maxsize=1)
result_queue = Queue(maxsize=1)
stop_event = threading.Event()
# ------------------------------
# 动态背景视频支持(优化版)
# ------------------------------
BG_VIDEO_PATH = "menu_bg.mp4"
bg_frames = [] # 预加载的帧列表
bg_lock = threading.Lock() # 帧访问锁
bg_stop_event = threading.Event() # 控制后台线程退出
# ------------------------------
# 初始化 Mediapipe Hands
# ------------------------------
mp_hands = mp.solutions.hands
mp_draw = mp.solutions.drawing_utils
hands = mp_hands.Hands(
static_image_mode=False,
max_num_hands=2,
min_detection_confidence=0.7,
min_tracking_confidence=0.5
)
# ------------------------------
# 中文字体加载
# ------------------------------
def get_chinese_font():
system = platform.system()
font_paths = {
"Windows": ["C:/Windows/Fonts/simhei.ttf", "C:/Windows/Fonts/msyh.ttc"],
"Darwin": ["/System/Library/Fonts/PingFang.ttc"],
"Linux": [
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc"
]
}
for path in font_paths.get(system, []):
if os.path.exists(path):
try:
return ImageFont.truetype(path, 32)
except Exception as e:
print(f"⚠️ 字体加载失败: {path}, 错误: {e}")
return None
try:
CHINESE_FONT = get_chinese_font()
except ImportError:
print("警告: 未安装Pillow,中文可能无法正常显示。请运行: pip install pillow")
CHINESE_FONT = None
def put_chinese_text(image, text, position=None, color=(255, 255, 255), font_size=30, center_region=None):
if CHINESE_FONT is None:
if position:
cv2.putText(image, text, position, cv2.FONT_HERSHEY_SIMPLEX, 1, color, 2)
return image
try:
pil_img = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(pil_img)
try:
if hasattr(CHINESE_FONT, 'path'):
font = ImageFont.truetype(CHINESE_FONT.path, font_size)
else:
font = ImageFont.load_default()
except:
font = ImageFont.load_default()
bbox = draw.textbbox((0, 0), text, font=font)
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
if center_region:
x1, y1, x2, y2 = center_region
fx = x1 + (x2 - x1 - tw) // 2
fy = y1 + (y2 - y1 - th) // 2
elif position:
fx, fy = position
else:
fx = fy = 0
draw.text((fx, fy), text, fill=tuple(color), font=font)
return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
except Exception as e:
print("中文绘制失败:", e)
if position:
cv2.putText(image, text, position, cv2.FONT_HERSHEY_SIMPLEX, 1, color, 2)
return image
# ------------------------------
# 音频系统(pygame.mixer)
# ------------------------------
pygame.mixer.init(frequency=22050, size=-16, channels=2, buffer=512)
_sound_cache = {}
_volume = 0.5
_muted = False
def _load_sound(file):
if file in _sound_cache:
return _sound_cache[file]
if os.path.exists(file):
try:
sound = pygame.mixer.Sound(file)
sound.set_volume(_volume)
_sound_cache[file] = sound
return sound
except Exception as e:
print(f"❌ 加载音效失败: {file}, 错误: {e}")
else:
print(f"⚠️ 音效文件未找到: {file}")
return None
def play_sound(sound_file):
if not _muted:
sound = _load_sound(sound_file)
if sound:
sound.play()
def play_bgm(bgm_file, loop=-1):
if _muted or not os.path.exists(bgm_file):
return
try:
pygame.mixer.music.load(bgm_file)
pygame.mixer.music.set_volume(_volume)
pygame.mixer.music.play(loop)
except Exception as e:
print(f"❌ 播放 BGM 失败: {e}")
def stop_bgm():
pygame.mixer.music.stop()
def toggle_mute():
global _muted
_muted = not _muted
vol = 0.0 if _muted else _volume
pygame.mixer.music.set_volume(vol)
for s in _sound_cache.values():
if s:
s.set_volume(vol)
return _muted
# ------------------------------
# 排行榜管理
# ------------------------------
RANKING_FILE = "snake_ranking.json"
MAX_RECORDS = 100
def load_ranking():
if not os.path.exists(RANKING_FILE): return []
try:
with open(RANKING_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
seen = set()
unique_data = []
for item in data:
key = (item.get('score', 0), item.get('timestamp', ''))
if key not in seen:
seen.add(key)
unique_data.append(item)
return sorted(unique_data, key=lambda x: x['score'], reverse=True)[:MAX_RECORDS]
except Exception as e:
print("加载排行榜失败:", e)
return []
def save_best_score(score, mode="single"):
ranking = load_ranking()
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
ranking.append({"score": score, "timestamp": current_time, "mode": mode})
seen = set()
unique_ranking = []
for r in ranking:
key = (r['score'], r['timestamp'])
if key not in seen:
seen.add(key)
unique_ranking.append(r)
unique_ranking = sorted(unique_ranking, key=lambda x: x['score'], reverse=True)[:MAX_RECORDS]
try:
with open(RANKING_FILE, 'w', encoding='utf-8') as f:
json.dump(unique_ranking, f, indent=2, ensure_ascii=False)
except Exception as e:
print("保存失败:", e)
def show_ranking():
ranking = load_ranking()
frame = np.zeros((720, 1280, 3), dtype=np.uint8)
h, w = frame.shape[:2]
for y in range(h):
c = 30 + y // 20
frame[y, :] = [c // 2, c // 3, c]
frame = put_chinese_text(frame, "🏆 历史排行榜 🏆", center_region=(0, 40, w, 120), color=(255, 255, 100),
font_size=60)
frame = put_chinese_text(frame, "按任意键返回主菜单", center_region=(0, h - 80, w, h), color=(200, 200, 255),
font_size=30)
if not ranking:
frame = put_chinese_text(frame, "暂无记录", center_region=(0, h // 2 - 20, w, h // 2 + 20),
color=(150, 150, 150), font_size=40)
else:
start_y = 140
for i, record in enumerate(ranking[:50]):
score = record['score']
timestamp = record['timestamp']
mode = record.get('mode', 'unknown')
mode_text = "单人" if mode == "single" else "双人" if mode == "dual" else "未知"
text = f"{i + 1:2d}. {score:4d} 分 — {timestamp} [{mode_text}]"
color = (255, 255, 255) if i % 2 == 0 else (220, 220, 220)
frame = put_chinese_text(frame, text, (w // 2 - 480, start_y + i * 32), color=color, font_size=28)
cv2.imshow("Hand-Controlled Snake Game", frame)
cv2.waitKey(0)
def clear_ranking():
frame = np.zeros((720, 1280, 3), dtype=np.uint8)
h, w = frame.shape[:2]
for y in range(h):
frame[y, :] = [20 + y // 20, (20 + y // 20) // 2, 60]
frame = put_chinese_text(frame, "⚠️ 清空所有记录?", center_region=(0, h // 2 - 100, w, h // 2 - 40),
color=(0, 0, 255), font_size=48)
frame = put_chinese_text(frame, "确定要清空吗?(Y=是, N=否)", center_region=(0, h // 2 - 20, w, h // 2 + 20),
color=(255, 255, 255), font_size=36)
frame = put_chinese_text(frame, "此操作不可撤销!", center_region=(0, h // 2 + 40, w, h // 2 + 80),
color=(150, 150, 150), font_size=32)
cv2.imshow("Hand-Controlled Snake Game", frame)
while True:
key = cv2.waitKey(1) & 0xFF
if key in (ord('y'), ord('Y')):
try:
os.remove(RANKING_FILE)
except:
pass
return True
elif key in (ord('n'), ord('N'), 27):
return False
# ------------------------------
# 动态背景视频相关函数(✅ 终极流畅版)
# ------------------------------
def load_background_video():
"""启动后台线程预加载并循环读取视频帧"""
global bg_frames
if not os.path.exists(BG_VIDEO_PATH):
print(f"❌ 背景视频未找到: {BG_VIDEO_PATH}")
return
def preload_and_cache():
cap = cv2.VideoCapture(BG_VIDEO_PATH)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
cap.set(cv2.CAP_PROP_FPS, 30)
if hasattr(cv2, 'CAP_PROP_HW_ACCELERATION'):
try:
cap.set(cv2.CAP_PROP_HW_ACCELERATION, cv2.VIDEO_ACCELERATION_ANY)
except:
pass
frame_cache = []
# 🔁 尽可能多地预加载帧(最多300帧 ≈ 10秒)
while len(frame_cache) < 300:
ret, frame = cap.read()
if not ret:
break
resized = cv2.resize(frame, (1280, 720), interpolation=cv2.INTER_AREA)
frame_cache.append(resized)
if not frame_cache:
print("❌ 无法解码任何背景帧")
return
print(f"✅ 成功预加载 {len(frame_cache)} 帧背景画面")
# 🔁 后台无限循环更新全局帧
while not bg_stop_event.is_set():
for frame in frame_cache:
with bg_lock:
if len(bg_frames) > 0:
bg_frames[0] = frame.copy() # 更新第一帧
else:
bg_frames.append(frame.copy())
time.sleep(1 / 30) # 模拟 ~30 FPS 输出节奏
if bg_stop_event.is_set():
break
cap.release()
# 🧵 启动预加载线程
thread = threading.Thread(target=preload_and_cache, daemon=True)
thread.start()
def get_background_frame():
"""从内存中快速获取当前背景帧"""
with bg_lock:
if bg_frames:
return bg_frames[0].copy() # 返回副本避免绘图冲突
return None # 降级为纯色背景
# ------------------------------
# 主菜单界面(带动态视频背景)
# ------------------------------
def show_main_menu():
global bg_frames
if len(bg_frames) == 0:
load_background_video()
play_bgm("bgm.mp3")
ranking = load_ranking()
best_score = ranking[0]['score'] if ranking else 0
width, height = 1280, 720
btn_w, btn_h = 300, 80
cx = width // 2
btn_y_start = 220
spacing = 80
while True:
bg_frame = get_background_frame()
if bg_frame is not None:
frame = bg_frame.copy()
frame = cv2.GaussianBlur(frame, (5, 5), 0)
frame = cv2.addWeighted(frame, 0.7, np.zeros_like(frame), 0, 30)
else:
frame = np.zeros((height, width, 3), dtype=np.uint8)
for y in range(height):
c = 20 + y // 20
frame[y, :] = [c, c // 2, 60]
frame = put_chinese_text(frame, "🐍 手势贪吃蛇游戏 🐍", center_region=(0, 0, width, 120),
color=(255, 255, 100), font_size=60)
buttons = [
("👤 单人模式", btn_y_start, (100, 0, 200), (140, 0, 240)),
("👥 双人对战", btn_y_start + spacing, (180, 0, 180), (220, 0, 220)),
("📊 查看排行榜", btn_y_start + 2 * spacing, (0, 100, 200), (0, 180, 255)),
("🗑️ 清空记录", btn_y_start + 3 * spacing, (60, 60, 60), (150, 150, 150)),
]
for label, y, bg_color, border_color in buttons:
left = cx - btn_w // 2
right = cx + btn_w // 2
top = y
bottom = y + btn_h
cv2.rectangle(frame, (left, top), (right, bottom), bg_color, -1)
cv2.rectangle(frame, (left, top), (right, bottom), border_color, 5)
frame = put_chinese_text(frame, label, center_region=(left, top, right, bottom),
color=(255, 255, 255), font_size=36)
tip_text = f"🏆 最高分: {best_score}|1=单人 2=双人 3=退出 4=清空 5=排行 M=静音"
frame = put_chinese_text(frame, tip_text, center_region=(0, 600, width, 640),
color=(200, 255, 255), font_size=24)
cv2.imshow("Hand-Controlled Snake Game", frame)
start_t = time.time()
while time.time() - start_t < 0.1:
key = cv2.waitKey(1) & 0xFF
if key == ord('1'):
return "single"
elif key == ord('2'):
return "dual"
elif key == ord('3'):
return "quit"
elif key == ord('4'):
clear_ranking()
break
elif key == ord('5'):
show_ranking()
break
elif key == ord('m'):
is_muted = toggle_mute()
status = "🔇 已静音" if is_muted else "🔊 音效开启"
help_frame = frame.copy()
help_frame = put_chinese_text(help_frame, status, center_region=(0, 650, width, 690),
color=(255, 255, 0), font_size=24)
cv2.imshow("Hand-Controlled Snake Game", help_frame)
cv2.waitKey(800)
break
elif key != 255:
help_frame = frame.copy()
help_frame = put_chinese_text(help_frame, "💡 提示: 1=单人 2=双人 3=退出 4=清空 5=排行 M=静音",
center_region=(0, 650, width, 690), color=(255, 255, 0), font_size=24)
cv2.imshow("Hand-Controlled Snake Game", help_frame)
cv2.waitKey(1500)
break
# ------------------------------
# 摄像头读取线程
# ------------------------------
def capture_thread():
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
while not stop_event.is_set():
ret, frame = cap.read()
if ret:
frame = cv2.flip(frame, 1)
if not frame_queue.empty():
try:
frame_queue.get_nowait()
except:
pass
frame_queue.put(frame)
else:
time.sleep(0.01)
cap.release()
# ------------------------------
# 手势识别线程
# ------------------------------
def hand_thread():
while not stop_event.is_set():
if not frame_queue.empty():
frame = frame_queue.get()
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
results = hands.process(rgb_frame)
if not result_queue.empty():
try:
result_queue.get_nowait()
except:
pass
result_queue.put((frame.copy(), results))
else:
time.sleep(0.01)
# ------------------------------
# 启动后台线程
# ------------------------------
capture_t = threading.Thread(target=capture_thread, daemon=True)
hand_t = threading.Thread(target=hand_thread, daemon=True)
capture_t.start()
hand_t.start()
# ------------------------------
# 游戏参数
# ------------------------------
width, height = 1280, 720
snake_speed = 8
food_radius = 15
food_color = (0, 255, 0)
# ------------------------------
# 安全工具函数
# ------------------------------
def is_point_in_wall(x, y, walls):
return any(wx <= x <= wx + ww and wy <= y <= wy + wh for (wx, wy, ww, wh) in walls)
def check_wall_collision(head, walls):
hx, hy = head
return any(wx <= hx <= wx + ww and wy <= hy <= wy + wh for (wx, wy, ww, wh) in walls)
def generate_safe_food_position(snake_bodies, walls, retries=100):
all_segments = []
for body in (snake_bodies if isinstance(snake_bodies, list) else [snake_bodies]):
all_segments.extend(list(body))
for _ in range(retries):
x = random.randint(50, width - 50)
y = random.randint(50, height - 50)
pos = np.array([x, y])
if is_point_in_wall(x, y, walls):
continue
too_close = False
for seg in all_segments:
if np.linalg.norm(pos - np.array(seg)) < 30:
too_close = True
break
if too_close:
continue
return [int(x), int(y)]
fallback_positions = [(width - 60, height - 60), (60, height - 60), (width - 60, 60), (60, 60)]
for fx, fy in fallback_positions:
pos = np.array([fx, fy])
if not is_point_in_wall(fx, fy, walls):
safe = True
for seg in all_segments:
if np.linalg.norm(pos - np.array(seg)) < 30:
safe = False
break
if safe:
return [fx, fy]
return [width // 2, height // 2]
def add_wall_safely(walls_list, snake_bodies, food_pos):
if len(walls_list) >= 5: return walls_list
all_snakes = []
for body in (snake_bodies if isinstance(snake_bodies, list) else [snake_bodies]):
all_snakes.extend(list(body))
for _ in range(30):
ww, wh = random.randint(40, 150), random.randint(40, 150)
wx, wy = random.randint(50, width - ww - 50), random.randint(50, height - wh - 50)
new_wall = (wx, wy, ww, wh)
temp_walls = walls_list + [new_wall]
if not is_point_in_wall(all_snakes[0][0], all_snakes[0][1], temp_walls) and \
not is_point_in_wall(food_pos[0], food_pos[1], temp_walls):
walls_list.append(new_wall)
break
return walls_list
# ------------------------------
# 绘图封装
# ------------------------------
def draw_game_elements(frame, snakes, food, walls, scores, game_over, right_hand_pos=None, left_hand_pos=None):
try:
for wall in walls:
wx, wy, ww, wh = map(int, wall)
cv2.rectangle(frame, (wx, wy), (wx + ww, wy + wh), (255, 255, 0), -1)
cv2.rectangle(frame, (wx, wy), (wx + ww, wy + wh), (0, 0, 0), 3)
fx, fy = map(int, food)
if 0 <= fx < width and 0 <= fy < height:
cv2.circle(frame, (fx, fy), food_radius, food_color, -1)
s1, s2 = snakes
sc1, sc2 = scores
for i in range(1, len(s1)):
p1, p2 = s1[i - 1], s1[i]
if all(0 <= v < 10000 for v in (*p1, *p2)):
cv2.line(frame, p1, p2, (0, 0, 200), 8)
cv2.circle(frame, s1[0], 10, (255, 255, 255), -1)
cv2.circle(frame, s1[0], 8, (0, 0, 255), -1)
if len(s2) > 0:
for i in range(1, len(s2)):
p1, p2 = s2[i - 1], s2[i]
if all(0 <= v < 10000 for v in (*p1, *p2)):
cv2.line(frame, p1, p2, (200, 0, 0), 8)
cv2.circle(frame, s2[0], 10, (255, 255, 255), -1)
cv2.circle(frame, s2[0], 8, (255, 0, 0), -1)
frame = put_chinese_text(frame, f"🔴红蛇得分: {sc1}", (20, 15), (255, 255, 255), 40)
if sc2 > 0:
frame = put_chinese_text(frame, f"🔵蓝蛇得分: {sc2}", (20, 60), (255, 255, 255), 40)
if right_hand_pos and all(0 <= v < width for v in right_hand_pos):
cv2.circle(frame, tuple(map(int, right_hand_pos)), 12, (0, 0, 255), -1)
cv2.putText(frame, "R", (right_hand_pos[0] - 10, right_hand_pos[1] - 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)
if left_hand_pos and all(0 <= v < width for v in left_hand_pos):
cv2.circle(frame, tuple(map(int, left_hand_pos)), 12, (255, 0, 0), -1)
cv2.putText(frame, "L", (left_hand_pos[0] - 10, left_hand_pos[1] - 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 0, 0), 2)
except Exception as e:
print("绘图异常:", e)
def show_game_over_screen_dual(score1, score2):
overlay = np.zeros((height, width, 3), dtype=np.uint8)
cv2.rectangle(overlay, (width // 4, height // 4), (3 * width // 4, 3 * height // 4), (0, 0, 0), -1)
overlay = put_chinese_text(overlay, "游戏结束", center_region=(0, height // 2 - 80, width, height // 2 - 20),
color=(0, 0, 255), font_size=60)
overlay = put_chinese_text(overlay, f"红蛇得分: {score1}", center_region=(0, height // 2, width, height // 2 + 40),
color=(0, 0, 255), font_size=32)
overlay = put_chinese_text(overlay, f"蓝蛇得分: {score2}",
center_region=(0, height // 2 + 40, width, height // 2 + 80), color=(255, 0, 0),
font_size=32)
cv2.imshow("Hand-Controlled Snake Game", overlay)
play_sound("game_over.wav")
save_best_score(max(score1, score2), mode="dual")
cv2.waitKey(800)
def show_game_over_screen_single(score):
overlay = np.zeros((height, width, 3), dtype=np.uint8)
cv2.rectangle(overlay, (width // 4, height // 4), (3 * width // 4, 3 * height // 4), (0, 0, 0), -1)
overlay = put_chinese_text(overlay, "游戏结束", center_region=(0, height // 2 - 60, width, height // 2 - 20),
color=(0, 255, 0), font_size=60)
overlay = put_chinese_text(overlay, f"你的得分: {score}", center_region=(0, height // 2, width, height // 2 + 40),
color=(255, 255, 255), font_size=40)
cv2.imshow("Hand-Controlled Snake Game", overlay)
play_sound("game_over.wav")
save_best_score(score, mode="single")
cv2.waitKey(800)
# ------------------------------
# 单人游戏循环
# ------------------------------
def run_single_player():
snake = deque([(width // 2, height // 2)])
direction = np.array([1.0, 0.0])
base_length = 3
score = 0
walls = []
food = generate_safe_food_position(snake, walls)
target_pos = None
game_over = False
while not game_over:
if not result_queue.empty():
frame, results = result_queue.get()
key = cv2.waitKey(1) & 0xFF
curr_target = None
try:
if results.multi_hand_landmarks and results.multi_handedness:
for idx, hand_landmarks in enumerate(results.multi_hand_landmarks):
label = results.multi_handedness[idx].classification[0].label
if label == "Right":
x = int(hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].x * width)
y = int(hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].y * height)
curr_target = (x, y)
mp_draw.draw_landmarks(frame, hand_landmarks, mp_hands.HAND_CONNECTIONS)
if curr_target:
target_pos = curr_target if target_pos is None else (
int(0.3 * curr_target[0] + 0.7 * target_pos[0]),
int(0.3 * curr_target[1] + 0.7 * target_pos[1])
)
if target_pos:
head = np.array(snake[0])
to_target = np.array(target_pos) - head
dist = np.linalg.norm(to_target)
if dist > 30:
desired_dir = to_target / (dist + 1e-8)
current_dir = direction / (np.linalg.norm(direction) + 1e-8)
dot = np.clip(np.dot(current_dir, desired_dir), -1, 1)
angle = np.arccos(dot)
if angle > 0.25:
w1, w2 = np.sin(angle - 0.25), np.sin(0.25)
blended = (w1 * current_dir + w2 * desired_dir) / (np.sin(angle) + 1e-8)
direction = blended / (np.linalg.norm(blended) + 1e-8)
else:
direction = desired_dir
new_head = head + direction * snake_speed
snake.appendleft(tuple(np.round(new_head).astype(int)))
h = np.array(snake[0])
if h[0] <= 0 or h[0] >= width or h[1] <= 0 or h[1] >= height or check_wall_collision(h, walls):
game_over = True
for i in range(4, len(snake)):
if np.linalg.norm(h - np.array(snake[i])) < 15:
game_over = True
break
if np.linalg.norm(h - np.array(food)) < (food_radius + 10):
score += 1
food = generate_safe_food_position(snake, walls)
play_sound("eat.wav")
if score % 5 == 0:
walls = add_wall_safely(walls, snake, food)
target_len = base_length + score
while len(snake) > target_len:
snake.pop()
draw_game_elements(frame, [snake, deque()], food, walls, (score, 0), game_over,
right_hand_pos=target_pos, left_hand_pos=None)
cv2.imshow("Hand-Controlled Snake Game", frame)
if game_over:
show_game_over_screen_single(score)
break
if key == 27:
break
except Exception as e:
print("单人模式异常:", e)
time.sleep(0.1)
else:
time.sleep(0.01)
# ------------------------------
# 主游戏循环
# ------------------------------
while True:
action = show_main_menu()
if action == "quit":
break
elif action == "single":
run_single_player()
elif action == "dual":
snake1 = deque([(width // 3, height // 2)])
dir1 = np.array([1.0, 0.0])
len1 = 3
snake2 = deque([(2 * width // 3, height // 2)])
dir2 = np.array([-1.0, 0.0])
len2 = 3
score1, score2 = 0, 0
walls = []
food = generate_safe_food_position([*snake1, *snake2], walls)
game_over = False
target_right = None
target_left = None
while not game_over:
if not result_queue.empty():
frame, results = result_queue.get()
key = cv2.waitKey(1) & 0xFF
curr_right = None
curr_left = None
try:
if results and results.multi_hand_landmarks and results.multi_handedness:
hands_info = []
for idx, hand_landmarks in enumerate(results.multi_hand_landmarks):
wrist_x = hand_landmarks.landmark[mp_hands.HandLandmark.WRIST].x
x = int(hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].x * width)
y = int(hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].y * height)
hands_info.append((wrist_x, x, y, hand_landmarks))
hands_info.sort(key=lambda x: x[0], reverse=True)
for i, (_, x, y, landmarks) in enumerate(hands_info):
mp_draw.draw_landmarks(frame, landmarks, mp_hands.HAND_CONNECTIONS)
if i == 0: curr_right = (x, y)
if i == 1: curr_left = (x, y)
if curr_right:
target_right = curr_right if target_right is None else (
int(0.3 * curr_right[0] + 0.7 * target_right[0]),
int(0.3 * curr_right[1] + 0.7 * target_right[1]))
if curr_left:
target_left = curr_left if target_left is None else (
int(0.3 * curr_left[0] + 0.7 * target_left[0]),
int(0.3 * curr_left[1] + 0.7 * target_left[1]))
if target_right:
head1 = np.array(snake1[0])
to_t = np.array(target_right) - head1
dist = np.linalg.norm(to_t)
if dist > 30:
desired = to_t / (dist + 1e-8)
curr_dir = dir1 / (np.linalg.norm(dir1) + 1e-8)
dot = np.clip(np.dot(curr_dir, desired), -1, 1)
angle = np.arccos(dot)
if angle > 0.25:
w1, w2 = np.sin(angle - 0.25), np.sin(0.25)
blended = (w1 * curr_dir + w2 * desired) / (np.sin(angle) + 1e-8)
dir1 = blended / (np.linalg.norm(blended) + 1e-8)
else:
dir1 = desired
new_head1 = head1 + dir1 * snake_speed
snake1.appendleft(tuple(np.round(new_head1).astype(int)))
if target_left:
head2 = np.array(snake2[0])
to_t = np.array(target_left) - head2
dist = np.linalg.norm(to_t)
if dist > 30:
desired = to_t / (dist + 1e-8)
curr_dir = dir2 / (np.linalg.norm(dir2) + 1e-8)
dot = np.clip(np.dot(curr_dir, desired), -1, 1)
angle = np.arccos(dot)
if angle > 0.25:
w1, w2 = np.sin(angle - 0.25), np.sin(0.25)
blended = (w1 * curr_dir + w2 * desired) / (np.sin(angle) + 1e-8)
dir2 = blended / (np.linalg.norm(blended) + 1e-8)
else:
dir2 = desired
new_head2 = head2 + dir2 * snake_speed
snake2.appendleft(tuple(np.round(new_head2).astype(int)))
h1, h2 = np.array(snake1[0]), np.array(snake2[0])
if (h1[0] <= 0 or h1[0] >= width or h1[1] <= 0 or h1[1] >= height or check_wall_collision(h1,
walls)):
game_over = True
for i in range(4, len(snake1)):
if np.linalg.norm(h1 - np.array(snake1[i])) < 15:
game_over = True
break
if (h2[0] <= 0 or h2[0] >= width or h2[1] <= 0 or h2[1] >= height or check_wall_collision(h2,
walls)):
game_over = True
for i in range(4, len(snake2)):
if np.linalg.norm(h2 - np.array(snake2[i])) < 15:
game_over = True
break
eaten = False
if np.linalg.norm(h1 - np.array(food)) < (food_radius + 10):
score1 += 1
eaten = True
if np.linalg.norm(h2 - np.array(food)) < (food_radius + 10):
score2 += 1
eaten = True
if eaten:
play_sound("eat.wav")
food = generate_safe_food_position([*snake1, *snake2], walls)
if score1 % 5 == 0 or score2 % 5 == 0:
walls = add_wall_safely(walls, [*snake1, *snake2], food)
while len(snake1) > len1 + score1:
snake1.pop()
while len(snake2) > len2 + score2:
snake2.pop()
draw_game_elements(frame, [snake1, snake2], food, walls, (score1, score2), game_over,
right_hand_pos=target_right, left_hand_pos=target_left)
cv2.imshow("Hand-Controlled Snake Game", frame)
if game_over:
show_game_over_screen_dual(score1, score2)
break
if key == 27:
break
except Exception as e:
print("双人模式异常:", e)
time.sleep(0.1)
else:
time.sleep(0.01)
# ------------------------------
# 释放资源
# ------------------------------
stop_event.set()
bg_stop_event.set() # 停止背景线程
time.sleep(0.5)
cv2.destroyAllWindows()
hands.close()
stop_bgm()
pygame.mixer.quit()
最新发布