
简介资源包为Python游戏编程课程设计大作业《与蛇共舞》AI贪吃蛇小游戏完整源码基于标准贪吃蛇逻辑引入百度AI人体关键点分析通过摄像头捕捉手臂、头部与肩部姿态将运动方向转化为上下左右控制指令并以socket127.0.0.1:20163实时驱动小蛇移动。适合Python入门者完成课程设计、项目实践或人工智能体验学习。内附开发文档、代码注释、PPT展示及可运行脚本主要文件类型有Python源码py、IPython笔记本ipynb、编译缓存pyc、演示图片与PPT、音乐素材等py与ipynb配合便于边阅读思路边调试运行。整个压缩包共43个文件大小约9.47MB目录结构清晰可按模块学习。已有936人学习下载除三大主程序摄像头姿态分析、标准贪吃蛇、音乐播放外还提供百度AI封装库、语音识别脚本、作品展示PPT和项目使用方法说明注释详细便于理解socket通信、多进程、姿态识别与游戏逻辑的结合上手即可运行并二次开发。1. 与蛇共舞把摄像头姿态识别接进贪吃蛇的课程设计这份《与蛇共舞》课程设计源码核心就一句话小蛇吃苹果我来做运动。摄像头拍下你的姿态百度 AI 平台做人体关键点分析程序根据手臂、头部和肩膀的相对位置判断上下左右再通过 socket 把方向喂给标准贪吃蛇程序。它既不是纯键盘游戏也不是算法 demo而是一条完整的 AI 交互链路摄像头采集、姿态识别、socket 传参、多进程接收、pgzero 渲染。卡住新手的往往不在贪吃蛇本身而在姿态到方向的换算以及 127.0.0.1:20163 那条数据链路。适合三类人找 Python 游戏编程课程设计大作业的在校生、想复现 AI 体感小游戏的入门者、想研究 AI 智能体感知交互链路的开发者。2. 三大模块与百度 AI 人体关键点camera_get_direct.py 的姿态采集和方向判定2.1 压缩包里到底有什么主程序、封装库与辅助示例先看压缩包结构避免一进来就被一堆 baidu_*.py 吓到。主程序只有三个camera_get_direct.py 负责摄像头和姿态识别snake.py 负责贪吃蛇本体play_music.py 负责背景音乐。真正参与游戏链路的只有这三个文件加 images 素材。文件 / 目录在项目里的角色camera_get_direct.py摄像头取帧、百度 AI 人体关键点识别、方向判定、socket 发送snake.py基于 pgzero 1.2 的贪吃蛇多进程接收方向并更新运动play_music.py用 pygame.mixer 循环播放 music_one.mp3baidu_api_lib/对百度 AI 平台接口的封装库来自史然飞《中学生都能玩的人工智能》配套封装baidu_speech_recognition.py、baidu_picture.py、baidu_couplets.py、voice_record.py同一封装库下的语音识别、图片识别、对联生成、录音示例images/、source_art/蛇头、蛇身、苹果、墙面的绘图素材music/演示用背景音乐run_snake.sh一键启动脚本按蛇端优先的顺序拉起三个程序作品展示.pptx、项目使用方法.txt、先读我答辩 PPT 和上手说明这些 baidu_*.py 不是主程序但值得翻一遍。它们展示了同一个 baidu_api_lib 分别调语音识别、图片识别、对联生成的写法游戏里的人体关键点识别只是这个封装的其中一个能力。读懂其中一个示例再回来看 camera_get_direct.py会发现调用模式完全一致造请求、传参、解析返回里的某个字段。根目录的「先读我」和项目使用方法.txt 里写了启动顺序和注意事项比我自己摸索时的经验还全。2.2 为什么选百度 AI 平台而不是本地人体姿态库做人体姿态识别常见方案有 MediaPipe、OpenPose也确实能本地跑但这两类库在课程设计场景里都不太合适。OpenPose 要下载模型文件、依赖 CMake 和显卡环境教室电脑装一遍就是一场灾难MediaPipe 虽然轻但版本和 Python 环境的兼容性问题也很容易把时间耗在装环境上。百度 AI 平台的人体关键点分析是纯 HTTP 请求机器能联网、有 API Key 就能用。史然飞那本书里的封装库把 access_token 获取、HTTP 请求、错误码处理都包掉了主程序只需要传 base64 图片和解析返回。课程设计量级每天几十次调用完全在免费额度内对演示来说省掉环境折腾把精力留在游戏交互上这是最合理的选型。2.3 camera_get_direct.py摄像头取帧与人体关键点请求这个文件的核心流程是打开摄像头 → 取一帧 → 编码成 base64 → 调封装库 → 解析 body_parts。先看取帧和请求部分import base64 import cv2 # 打开默认摄像头0 表示第一路摄像头设备 cap cv2.VideoCapture(0) ret, frame cap.read() if not ret: print(摄像头取帧失败先检查设备占用或权限) cap.release() exit(1) # 百度 AI 图片类接口统一收 base64 字符串 _, img_encoded cv2.imencode(.jpg, frame) img_base64 base64.b64encode(img_encoded).decode(utf-8) # 调用人体关键点识别方法名以封装库实际版本为准 resp baidu_ai.body_analysis(img_base64) # 关键点挂在 body_info[0].body_parts 下 parts resp[body_info][0][body_parts] for name, point in parts.items(): print(name, point[x], point[y], score%.2f % point[score])逻辑说明cv2 负责取帧imencode 把 BGR 帧转成 jpg 再 base64 编码这是百度 AI 图片接口要求的输入格式。baidu_ai 对象来自 baidu_api_lib 封装库它内部已经处理了 access_token主程序不用关心鉴权细节。返回的 body_parts 是一个字典每个关键点有 x、y、score 三个值x、y 是像素坐标score 是置信度。参数说明if not ret 这个分支一定要保留。教室电脑摄像头被会议软件占用时read 会返回 False不处理的话后面 imencode 会拿到空数据直接抛异常。body_info[0] 表示识别到的第一个人画面里同时出现两个人时程序只取第一个演示时要提醒自己站在画面中央偏前的位置别让评委或其他同学进入画面。2.4 方向判定手臂、头部与肩膀的几何关系拿到关键点之后最关键的一步是把坐标换算成上下左右。项目摘要里说的是「通过姿态信息里手臂、头部和肩膀的关系」得到方向键常见做法是把画面划分成区域以头部 y 坐标为上界参考、肩膀 y 坐标为下界参考、脖子 x 坐标为中轴手腕落在哪个区域就发哪个方向。def calc_direction(parts): head parts.get(head, {}) neck parts.get(neck, {}) left_wrist parts.get(left_wrist, {}) right_wrist parts.get(right_wrist, {}) # 两只手都在画面里时取置信度更高的那只做判断 if left_wrist.get(score, 0) right_wrist.get(score, 0): wrist left_wrist else: wrist right_wrist if wrist.get(score, 0) 0.5 or head.get(score, 0) 0.5: return None # 垂直方向手腕相对头部的位置 dy wrist[y] - head[y] # 水平方向手腕相对脖子中轴的偏移 dx wrist[x] - neck[x] if abs(dy) 40: # 手腕和头差不多高按水平偏移区分左右 if dx 30: return right if dx -30: return left return None if dy -30: return up # 手已举过头顶 if dy 80: return down # 手明显低于头部接近肩线以下 return None判定逻辑说明dy wrist.y - head.y图像坐标系 y 向下增大所以 dy 为负表示手腕在头顶上方判 updy 很大表示手腕垂在身体下方判 down。水平方向用 neck 的 x 做中轴因为脖子基本在画面中线附近比用 head 的 x 更稳定。手腕高度和头接近时按 dx 区分左右。参数说明30、40、80 这些阈值是经验和演示距离的产物。摄像头离人 1.5 到 2 米、人站直时基本可用摄像头离人很近或人坐下来必须现场调。调法很简单分别举手、放平、垂手打印 dy、dx 看实际数值范围再把阈值设在中间。score 过滤放在最前面低于 0.5 的关键点说明百度 AI 自己都不确定别拿它当依据。3. socket 数据链路127.0.0.1:20163 上的方向传递与进程解耦3.1 为什么是 socket两条独立进程的边界很多人第一次看这个项目会问摄像头程序直接调用 snake 里的函数改方向不行吗不行。两个程序是独立进程各有各的生命周期摄像头程序要一直抢摄像头设备、循环取帧蛇程序要一直跑渲染循环谁都不能阻塞谁。放进同一个进程要么取帧卡住渲染要么渲染卡住取帧。用 socket 把两个进程解耦之后各自启动、各自崩溃、互不影响调试时还能单独把某一段跑起来验证。127.0.0.1 是回环地址只在本机通信不经过外网课程设计场景足够。20163 是双方约定的端口两个程序里写同一个端口就能对上。为什么不用 UDPUDP 丢包在体感场景确实可以容忍但 TCP 写起来也就多两行 connect 和 close而且答辩时 TCP 的「连接-发送-关闭」链路更容易被评委理解。这套方案本质是感知、决策、执行三段式的闭环摄像头取帧是感知方向判定是决策蛇的移动是执行这也是最简形态的 AI 智能体。3.2 发送端camera_get_direct.py 的方向派发发送端做的是短连接每判定出一个方向就新建 socket 连一次、发完立刻关闭。这样发送端永远不需要处理对端掉线的状态游戏端没启动时 connect 失败跳过这一帧就行不会拖慢取帧循环。import socket VALID_DIRECTIONS (up, down, left, right) def send_direction(direction): 方向字符串通过 TCP 短连接发给蛇端 if direction not in VALID_DIRECTIONS: return s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(1) # 连接失败最多等 1 秒避免卡住取帧循环 try: s.connect((127.0.0.1, 20163)) s.sendall(direction.encode(utf-8)) except OSError as e: # 蛇端没启动或端口被占本帧丢弃即可 print(方向发送失败:, e) finally: s.close()逻辑说明每判定完一个方向就调用一次 send_direction循环回到取帧继续下一轮。settimeout(1) 是给 connect 设置的超时上限本机回环正常情况下毫秒级完成设 1 秒只是为了防极端情况卡住取帧循环。finally 里的 s.close() 保证连接一定释放不会随着循环越积越多。参数说明VALID_DIRECTIONS 白名单在发送端和接收端各有一份这是双保险——发送端不发送非法字符串接收端也只认这四个方向任何杂数据都会被丢弃。方向字符串固定用 up/down/left/right不要改成中文或数字socket 传的是字节流统一编码省去一堆编码问题。3.3 接收端snake.py 里的多进程常驻监听接收端不能放在 pgzero 的 update 回调里。update 每帧都跑而 accept 是阻塞调用放进去游戏直接卡死。项目摘要里写明 snake.py 是「新建了一个进程」来做这件事这是标准做法import multiprocessing as mp import socket DIRECTION_CODE {up: 0, down: 1, left: 2, right: 3} CODE_TO_DIRECT {0: up, 1: down, 2: left, 3: right} def direction_listener(holder, stop_event): 独立进程常驻监听 20163收到合法方向就编码写入共享变量 server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((127.0.0.1, 20163)) server.listen(1) # 同一时刻只服务一个发送端 while not stop_event.is_set(): try: conn, _ server.accept() data conn.recv(16).decode(utf-8).strip() if data in DIRECTION_CODE: holder.value DIRECTION_CODE[data] conn.close() except OSError: break # 主程序入口 if __name__ __main__: holder mp.Value(i, 0) # 进程间共享的方向编码初始 0 对应 up stop_event mp.Event() listener mp.Process( targetdirection_listener, args(holder, stop_event), daemonTrue ) listener.start()逻辑说明direction_listener 在独立进程里死循环 accept每收到一个合法方向就编码写入 holder。holder 是 multiprocessing.Value(i)整型共享变量读写都带锁、是原子操作用整数编码而不是字符串共享是为了避开 ctypes 字符串类型在部分 Python 版本下的长度和同步坑。主进程的 update 每帧读 holder读到新值就换向。daemonTrue 保证游戏窗口关闭时监听进程跟着退出不会留下孤儿进程占着 20163 端口。参数说明listen(1) 表示内核只排一个等待连接本机单发送端场景够用。recv(16) 一次最多读 16 字节方向字符串最长 5 个字符right绰绰有余。SO_REUSEADDR 是血泪教训换来的不加它蛇端进程刚退出紧接着又启动时TIME_WAIT 状态会让 bind 报 Address already in use加上之后基本不撞。另外注意监听端永远只能有一份。摄像头端如果也写成 bindlisten蛇端先启动时摄像头端 connect 能连上但反过来蛇端没启动、摄像头端先跑蛇端后面 bind 就会失败。这个先后顺序问题第 5 章有对应的排错方法。4. 改造 pgzero 贪吃蛇把键盘方向源换成多进程共享变量4.1 pgzero 1.2 的运行方式和 snake 基础结构pgzero 是 Pygame Zero 的简称它把 pygame 的样板代码收掉只要求开发者实现 update 和 draw 两个回调。运行方式是 pgzrun snake.py或者 python snake.py文件末尾调用了 pgzrun.go()。1.2 版本对 Python 3.8 到 3.10 都友好安装用 pip install pgzero1.2 即可装不上先确认 Python 版本对不对这是最省心的环境组合。原版 snake 的方向来源是键盘事件on_key_down 里按上下左右改 direction要做 AI 控制本质就是把 direction 的写入者从「键盘事件」换成「socket 共享变量」。项目正文也写明「在 pgzero 1.2 的 snake 基础上」改造照着这个基准继续改最稳。4.2 共享变量的读取、反向过滤与运动节拍update 里读共享变量并更新方向是整个改造的核心。注意两点一是反向过滤二是用帧计数控制移动节拍。import pgzrun from pgzero.constants import keyboard SPEED 10 # 每 10 帧移动一格越小越快 DIRECTIONS { up: (0, -1), down: (0, 1), left: (-1, 0), right: (1, 0), } def update(): global direction, move_timer # 读共享变量里的方向编码AI 优先键盘兜底 ai_dir CODE_TO_DIRECT.get(holder.value) if ai_dir in DIRECTIONS: cur DIRECTIONS[direction] new DIRECTIONS[ai_dir] # 禁止 180 度掉头正在向右时不允许直接向左 if (new[0], new[1]) ! (-cur[0], -cur[1]): direction ai_dir # 按节拍移动而不是每帧都移 move_timer 1 if move_timer SPEED: move_timer 0 move_snake()逻辑说明holder.value 是整数编码用 CODE_TO_DIRECT 字典映射回方向字符串。方向合法性校验在写入共享变量时已经做了一轮update 里再做一轮是双保险holder 初始值是 0 映射 up而且多进程写入无法保证完全干净。move_timer 是帧计数达到 SPEED 才走一格这样蛇的移动速度、方向更新频率和 AI 判定的节奏解耦不会因为识别模块连续发方向导致蛇瞬移。参数说明反向过滤用向量相加判断新方向和当前方向的 x、y 分量互为相反数就是掉头直接忽略。SPEED10 在 60 帧渲染下约 0.17 秒走一格体感中等偏快现场手跟不上就把 SPEED 调到 12 或 14只改这一处不影响其它逻辑。4.3 move_snake 和吃苹果判定贪吃蛇本体逻辑沿用 pgzero 原版思路蛇身是一个坐标列表头部每步按 direction 加一个格子尾部去掉一格吃到苹果就保留尾巴实现变长。AI 控制这一层完全不插手位移只改 direction所以原版移动代码可以原样保留。def move_snake(): global snake, food, score head snake[0] dx, dy DIRECTIONS[direction] new_head (head[0] dx, head[1] dy) # 撞墙或撞自己判定 if new_head[0] 0 or new_head[0] GRID_WIDTH: game_over() return if new_head[1] 0 or new_head[1] GRID_HEIGHT: game_over() return if new_head in snake: game_over() return snake.insert(0, new_head) if new_head food: score 1 spawn_food() # 尾巴这一帧不删蛇就变长了 else: snake.pop()逻辑说明snake 用元组列表表示索引 0 是蛇头。新头插入列表头部没吃到苹果就 pop 掉尾部一格吃到苹果不 pop长度自然加一。撞墙和撞自己是独立的 game_over 触发条件。AI 控制时反向过滤已经挡掉了最危险的掉头剩余风险主要是人反应慢导致蛇撞墙。参数说明GRID_WIDTH 和 GRID_HEIGHT 是网格数pgzero 原版默认 20x15 左右与窗口像素映射后在 draw 里画格子。这两个参数不需要为 AI 改动保持原版即可。真正要关注的是 SPEED体感控制比键盘慢一拍速度越快越容易翻车。4.4 背景音乐模块play_music.py 的接入方式音乐播放是第三块独立程序用 pygame.mixer 播放 music/music_one.mp3。它不参与 socket 链路也不读取共享变量纯粹是演示气氛组。常见做法是import pygame def play_bgm(pathmusic/music_one.mp3): try: pygame.mixer.init() pygame.mixer.music.load(path) pygame.mixer.music.play(-1) # -1 表示循环播放 except pygame.error as e: # 教室机器没声卡或音频设备被占用不影响游戏主程序 print(背景音乐播放失败:, e)逻辑说明mixer.init 初始化音频设备load 加载 mp3play(-1) 循环。try/except 包住整段的意义在于答辩机器经常没有音频设备如果这段不兜底音乐没响还把游戏主程序拖崩现场就尴尬了。参数说明路径用相对路径 music/music_one.mp3前提是工作目录在项目根目录。用 run_snake.sh 启动时工作目录是脚本所在目录没问题手动在 IDE 里运行 snake.py 时一定要确认当前工作目录否则路径找不到音乐会静默失败而这个失败不会报错很难察觉。5. 常见问题与避坑五个必踩的坑和对应排查手段5.1 百度 AI 返回 error_code程序直接退出现象和原因一运行 camera_get_direct.py控制台就打印一串 error_code最常见的是 110 和 100程序直接退出。几乎都是同一个原因baidu_api_lib 里配置的 API Key、Secret Key 还是作者原有的或者 access_token 已经过期。百度 AI 的 token 有效期约 30 天课程设计做完隔一个多月才答辩的情况很常见过期了就是这个表现。解决到百度 AI 平台控制台新建应用拿到自己的 AK/SK填进封装库对应的配置位置。封装库如果带了刷新 token 的函数重跑一次即可没有就删掉本地 token 缓存文件再跑。答辩前一晚务必从头走一遍这个流程把它当成固定检查项这是最稳的后悔药。5.2 蛇端启动就报 Address already in use现象和原因snake.py 刚启动控制台立刻报 [Errno 98] Address already in use绑定 20163 失败。通常两个原因上一次运行留下的进程没退干净或者摄像头端被错误地配成了 listener蛇端再 bind 同一个端口就撞上。解决先确认启动顺序——必须蛇端先起来占端口摄像头端后起来做连接方。反复出现占用时Windows 用 netstat -ano | grep 20163 看 PIDLinux 用 lsof -i:20163找到 PID 直接 kill。socket 上加的 SO_REUSEADDR 能缓解 TIME_WAIT 造成的占用但解决不了进程还活着的情况。5.3 蛇突然 180 度掉头撞到自己身上现象和原因蛇正常向右走收到一个 left 方向后直接反向一头撞上自己身体游戏结束。原因是 socket 传来的方向被直接赋值给 directionupdate 里没做反向过滤。体感操作时手从右边甩向左边中间会经过 left 判定区间这个方向恰好与当前方向互逆。解决在 update 里按 4.2 的向量判断新方向是当前方向的反向就丢弃。这是整个改造里最容易漏的一步也是翻车率最高的一步。顺带提醒键盘兜底事件里也要加同样的过滤否则演示时按错方向一样自杀。这个过滤逻辑放哪都行就是不能没有。5.4 手停在原地方向却在 up、left 之间反复横跳现象和原因手保持一个姿势没动蛇的方向一直在抖左右摇摆。原因是单帧判定没有去抖手腕坐标在阈值边界附近轻微抖动score 也忽高忽低另一个可能是 score 过滤阈值设太低百度 AI 的误检点被当成手腕参与判定。解决常见做法是加投票窗口维护最近 5 帧的判定结果出现 3 次以上才派发history [] def vote(direction): 最近 5 帧中出现 3 次以上的方向才放行低于阈值的抖动被滤掉 history.append(direction) if len(history) 5: history.pop(0) if history.count(direction) 3: return direction return None逻辑说明每次判定结果先进入 history超过 5 帧就把最旧的弹出去然后统计当前方向出现次数达到 3 才返回否则返回 None。这样单帧误检会被稀释不会直接传导到蛇身。参数说明窗口 5 帧、阈值 3 次在 60 帧下大约 0.08 秒的判定窗口体感上几乎无延迟。如果抖动还明显把窗口加到 7、阈值提到 4。我一般先把 score 过滤提到 0.6再叠加这个投票抖动基本消失。5.5 摄像头黑屏或取帧失败现象和原因cv2.VideoCapture(0) 返回 False或者画面全黑、卡在某一帧。原因是教室电脑上腾讯会议、钉钉、OBS 等程序占用了摄像头设备或者笔记本的摄像头权限被系统设置拦掉。解决先关掉所有可能占用摄像头的软件再跑。Windows 在「设置-隐私-摄像头」里允许桌面应用访问macOS 在系统设置的摄像头权限里勾上终端或 Python。排查思路很关键先单独跑一个只取帧的脚本如果单独跑也黑就是设备或权限问题和游戏代码无关。调试阶段可以用本地视频文件代替摄像头把 VideoCapture(0) 换成 VideoCapture(test.mp4)后面的识别和 socket 链路照样能验证。6. 演示前自检用 mock 方向脚本验证整条链路答辩现场翻车率最高的不是识别不准而是链路没通。我在每次演示前都会做一件事不接摄像头用 mock 脚本直接往 20163 发方向单独验证蛇端的 socket、共享变量、update 读取这一段。import socket import time # 模拟摄像头端每 2 秒发一个方向蛇应按「上-左-下-右」转圈 for direction in [up, left, down, right, up]: s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((127.0.0.1, 20163)) s.sendall(direction.encode(utf-8)) s.close() print(sent, direction) time.sleep(2)逻辑说明脚本逐一发送方向每发一个停 2 秒方便肉眼观察蛇的转向。参数说明sleep(2) 是观察窗口链路没通时蛇会停在原地或只朝一个方向走不会瞬移连接方式与 camera_get_direct.py 完全一致都是短连接。先用 run_snake.sh 起蛇端再跑这个脚本蛇按顺序转圈说明链路全通不动就只在 snake.py 的 listener 或 holder 读取里找问题把摄像头和百度 AI 完全排除在外。链路验证完再做姿态校准分别举手、平举、垂手打印 dy、dx 实际范围把 2.4 里的阈值调到中间然后才正式演示。提示mock 脚本跑通后再起摄像头程序能把「链路问题」和「识别问题」彻底分开这是排错的第一步。现场启动顺序我固定是蛇端最先它要 bind 20163摄像头端其次连接方晚起无影响音乐最后音频设备出问题也不拖核心链路。摄像头离人 1.5 到 2 米正面打光别让其他人进入画面被误识别成第二个人。这些流程在源码包里的 run_snake.sh 和项目使用方法.txt 都有对应说明完整源码、注释和答辩 PPT 都在压缩包里做课程设计的可以直接按这份流程复现。从那以后我每次演示前都强制走一遍 mock 脚本再按蛇端优先的顺序启动确认蛇先动起来才把脸凑到摄像头前。这套流程帮我躲过了至少三次答辩事故希望帮到你。本文还有配套的精品资源点击获取