使用Python开发贪吃蛇游戏 SnakeGame


Posted in Python onApril 30, 2022

一、前言

想必大家都玩过贪吃蛇的游戏吧:通过操纵蛇的移动方向能够让蛇吃到随机出现的食物,吃到的食物越多,蛇就会变得越长,但如果不小心撞到了自己,那么蛇就会死亡,game over!! 我们玩过的贪吃蛇游戏一般都是在手机或者游戏机上进行的,通过方向键操纵蛇的移动,那么我们是否可以直接使用一个摄像头捕捉我们的手势动作,并用手的移动来代表贪吃蛇的移动呢?当然可以,今天我就和大家一起完成这个游戏的设计并愉快的玩耍。

Let's Start!

二、项目介绍

1、游戏的操作方式

贪吃蛇游戏人尽皆知,计算机视觉鲜为人知,计算机视觉+贪吃蛇游戏会带给人们更多的参与感以及新鲜度,本次这个项目就是主要使用手势识别来完成贪吃蛇这个简单的游戏。在这个游戏中,电脑通过摄像头捕捉到我们的手势并判别是否进行移动,玩家移动手去操纵贪吃蛇得到屏幕中随机出现的食物,每得到一个食物,就会算作一分,Score 就会加1并显示在画面中,当玩家在操作的过程中不小心使得蛇的头部和身体相撞,那么就会显示GameOver! 按下 ‘r’ 键可以重新开始游戏。

使用Python开发贪吃蛇游戏 SnakeGame

2、开发的过程中的注意事项

(1) 图像的左右问题

由于我们是使用手势来进行控制蛇的移动的,但摄像头的画面显示的是别人的视角,所以这和玩家的左右意识刚好是相反的,因此我们要将摄像头读取到的画面进行一个左右的翻转。原理上说就是将左右的像素点位置进行一个调换,但在 Python 中可以使用一个 cv2.flip( ) 函数就可以实现镜像翻转了。

(2) 摄像头的画面尺寸问题

过摄像头得到的图像我们需要在上面进行游戏,因此画面过小会导致游戏空间不足,在最开始可以对画面的大小进行一个预处理,设定一个较为合理的大小,最后得到的画面玩游戏时才不会显得局促。通过函数 cap.set(3, m) cap.set(4, n) 可以实现对画面的宽和高的设定。

本项目中还会存在一些其他的注意事项,比如判断碰撞,判断获得食物等,我会在后面的项目过程中再加以介绍。

三、游戏的实现要点

1、选择第三方库

一些使用到的第三方库:

import math
import random
import cvzone
import cv2
import numpy as np
from cvzone.HandTrackingModule import HandDetector

在本次项目中,我们主要使用到以上的几个库,其中使用 random 库来随机选择像素点来放置食物甜甜圈,使用 cvzone 中的手部识别来进行玩家手势的检测,使用 cv2 来进行一些基础的图像操作,其他的一些库也各有用处,后面一一介绍。

2、找到关键点并标记

在本次游戏中我们是选择了一只手作为目标节点,所以当我们检测到画面中出现手部时需要对其中的关键点进行标记,而这个关键点恰好是我们的贪吃蛇的头部,由于我们是调用的第三方库,而该库可以对手部进行3D的标记,但我们只需要 x,y 两个坐标值就可以了,主要使用以下函数进行手部关键节点的标记:

#检测到第一个手,并标记手部位置
    if hands:
        lmList = hands[0]['lmList']
        pointIndex = lmList[8][0:2] #第八个坐标点的 x, y值,其中 z 值不被包括在里面
        cv2.circle(img, pointIndex, 20, (200, 0, 200), cv2.FILLED) #在关键点处绘制一个圆点并进行填充(此处只是示范,后面会更改)

3、创建一个类来保存关于游戏的所有功能

我们需要实现的游戏是很多功能结合起来完成的,如果想要使用函数来实现这些功能,那么将会非常麻烦,当我们使用 class 来完成时,由于很多东西都保存在同一个类中,将会降低难度。在这个 class 中我们将会创建很多重要的列表来存储我们用得到的一些关键点,比如贪吃蛇的身上的所有的点、贪吃蛇的长度、蛇的总体距离、食物的放置、得分等:

class SnakeGameClass:
    def __init__(self, pathFood):
        self.points = []  #贪吃蛇身上所有点
        self.lengths = []  #点与点之间的距离
        self.currentLength = 0  #当下蛇的长度
        self.allowedLength = 50  #最大允许长度(阈值)
        self.previousHead = 0, 0  #手部关键点之后的第一个点
 
        self.imgFood = cv2.imread(pathFood, cv2.IMREAD_UNCHANGED) #定义食物
        self.hFood, self.wFood, _ = self.imgFood.shape
        self.foodPoint = 0, 0
        self.randomFoodLocation()
 
        self.score = 0
        self.gameOver = False

4、定义函数进行不断更新 

随着我们的手部的移动,贪吃蛇的长度以及位置都会发生变化,所以我们需要创建一个函数来不断进行更新,满足变化的需求(该部分也是在前面创建的大类里面完成的):

def update(self, imgMain, currentHead):
        #游戏结束,打印文本
        if self.gameOver:
            cvzone.putTextRect(imgMain, "Game Over", [300, 400],
                               scale=7, thickness=5, offset=20)
            cvzone.putTextRect(imgMain, f'Your Score: {self.score}', [300, 550],
                               scale=7, thickness=5, offset=20)
        else:
            px, py = self.previousHead
            cx, cy = currentHead
 
            self.points.append([cx, cy])
            distance = math.hypot(cx - px, cy - py)
            self.lengths.append(distance)
            self.currentLength += distance
            self.previousHead = cx, cy
 
            #长度缩小
            if self.currentLength > self.allowedLength:
                for i, length in enumerate(self.lengths):
                    self.currentLength -= length
                    self.lengths.pop(i)
                    self.points.pop(i)
                    if self.currentLength < self.allowedLength:
                        break
 
            #检查贪吃蛇是否已经触碰到食物
            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()
                self.allowedLength += 50
                self.score += 1
                print(self.score)
 
            #使用线条绘制贪吃蛇
            if self.points:
                for i, point in enumerate(self.points):
                    if i != 0:
                        cv2.line(imgMain, self.points[i - 1], self.points[i], (0, 0, 255), 20)
                cv2.circle(imgMain, self.points[-1], 20, (0, 255, 0), cv2.FILLED)
 
            #显示食物
            imgMain = cvzone.overlayPNG(imgMain, self.imgFood,
                                        (rx - self.wFood // 2, ry - self.hFood // 2))
 
            cvzone.putTextRect(imgMain, f'Score: {self.score}', [50, 80],
                               scale=3, thickness=3, offset=10)
 
            #检测是否碰撞
            pts = np.array(self.points[:-2], np.int32)
            pts = pts.reshape((-1, 1, 2))
            cv2.polylines(imgMain, [pts], False, (0, 255, 0), 3)
            minDist = cv2.pointPolygonTest(pts, (cx, cy), True)
 
            if -1 <= minDist <= 1:
                print("Hit")
                self.gameOver = True
                self.points = []  #蛇身上所有的点
                self.lengths = []  #不同点之间的距离
                self.currentLength = 0  #当前蛇的长度
                self.allowedLength = 50  #最大允许长度
                self.previousHead = 0, 0  #先前的蛇的头部
                self.randomFoodLocation()
 
        return imgMain

在这个更新的函数中,我们需要判断很多东西,比如贪吃蛇是否触碰到食物(如果触碰到食物我们就要增加蛇的长度并累积得分)、当前长度是否超过所允许的最大长度(当前长度小于最大长度就不必要进行更改了,但如果当前长度大于最大长度,则需要进行缩短)、贪吃蛇是否发生碰撞(通过关键节点之间的距离判断贪吃蛇是否发生了碰撞,如果发生了碰撞,则进入 gameover 模块,如果没有,继续游戏)等,都解释在上面的代码中了。

主要是通过上面定义的 class 我们就能实现当前的贪吃蛇游戏了。

四、总体代码

本次小游戏我是在b站看到教程并一步步复现出来的,大家感兴趣可以尝试一下,当然按照惯例整体代码会贴在下面:

"""
Author:XiaoMa
CSDN Address:一马归一码
"""
import math
import random
import cvzone
import cv2
import numpy as np
from cvzone.HandTrackingModule import HandDetector
 
cap = cv2.VideoCapture(0)
 
#设置画面的尺寸大小,过小的话导致贪吃蛇活动不开
cap.set(3, 1280)
cap.set(4, 720)
 
detector = HandDetector(detectionCon=0.8, maxHands=1)
 
 
class SnakeGameClass:
    def __init__(self, pathFood):
        self.points = []  #贪吃蛇身上所有点
        self.lengths = []  #每一个点之间的距离
        self.currentLength = 0  #当下蛇的长度
        self.allowedLength = 50  #最大允许长度(阈值)
        self.previousHead = 0, 0  #手部关键点之后的第一个点
 
        self.imgFood = cv2.imread(pathFood, cv2.IMREAD_UNCHANGED) #定义食物
        self.hFood, self.wFood, _ = self.imgFood.shape
        self.foodPoint = 0, 0
        self.randomFoodLocation()
 
        self.score = 0
        self.gameOver = False
 
    def randomFoodLocation(self):
        self.foodPoint = random.randint(100, 1000), random.randint(100, 600)
 
    def update(self, imgMain, currentHead):
        #游戏结束,打印文本
        if self.gameOver:
            cvzone.putTextRect(imgMain, "Game Over", [300, 400],
                               scale=7, thickness=5, offset=20)
            cvzone.putTextRect(imgMain, f'Your Score: {self.score}', [300, 550],
                               scale=7, thickness=5, offset=20)
        else:
            px, py = self.previousHead
            cx, cy = currentHead
 
            self.points.append([cx, cy])
            distance = math.hypot(cx - px, cy - py)
            self.lengths.append(distance)
            self.currentLength += distance
            self.previousHead = cx, cy
 
            #长度缩小
            if self.currentLength > self.allowedLength:
                for i, length in enumerate(self.lengths):
                    self.currentLength -= length
                    self.lengths.pop(i)
                    self.points.pop(i)
                    if self.currentLength < self.allowedLength:
                        break
 
            #检查贪吃蛇是否已经触碰到食物
            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()
                self.allowedLength += 50
                self.score += 1
                print(self.score)
 
            #使用线条绘制贪吃蛇
            if self.points:
                for i, point in enumerate(self.points):
                    if i != 0:
                        cv2.line(imgMain, self.points[i - 1], self.points[i], (0, 0, 255), 20)
                cv2.circle(imgMain, self.points[-1], 20, (0, 255, 0), cv2.FILLED)
 
            #显示食物
            imgMain = cvzone.overlayPNG(imgMain, self.imgFood,
                                        (rx - self.wFood // 2, ry - self.hFood // 2))
 
            cvzone.putTextRect(imgMain, f'Score: {self.score}', [50, 80],
                               scale=3, thickness=3, offset=10)
 
            #检测是否碰撞
            pts = np.array(self.points[:-2], np.int32)
            pts = pts.reshape((-1, 1, 2))
            cv2.polylines(imgMain, [pts], False, (0, 255, 0), 3)
            minDist = cv2.pointPolygonTest(pts, (cx, cy), True)
 
            if -1 <= minDist <= 1:
                print("Hit")
                self.gameOver = True
                self.points = []  #蛇身上所有的点
                self.lengths = []  #不同点之间的距离
                self.currentLength = 0  #当前蛇的长度
                self.allowedLength = 50  #最大允许长度
                self.previousHead = 0, 0  #先前的蛇的头部
                self.randomFoodLocation()
 
        return imgMain
 
 
game = SnakeGameClass("Donut.png")
 
while True:
    success, img = cap.read()
    img = cv2.flip(img, 1) #镜像翻转
    hands, img = detector.findHands(img, flipType=False)
    #检测到第一个手,并标记手部位置
    if hands:
        lmList = hands[0]['lmList']
        pointIndex = lmList[8][0:2] #第八个坐标点的 x, y值,其中 z 值不被包括在里面
        #cv2.circle(img, pointIndex, 20, (200, 0, 200), cv2.FILLED) #在关键点处绘制一个圆点并进行填充(此处只是示范,后面会更改)
        img = game.update(img, pointIndex)
 
    cv2.imshow("Image", img)
    key = cv2.waitKey(1)
    #按下‘r'重新开始游戏
    if key == ord('r'):
        game.gameOver = False

至于需要使用到的甜甜圈的图案,可以网上找一个合适的大小的图案进行替代即可。

使用Python开发贪吃蛇游戏 SnakeGame

到此这篇关于Python利用手势识别实现贪吃蛇游戏的文章就介绍到这了!


Tags in this post...

Python 相关文章推荐
Python抓取京东图书评论数据
Aug 31 Python
Python打印斐波拉契数列实例
Jul 07 Python
深入浅析python定时杀进程
Jun 06 Python
Zabbix实现微信报警功能
Oct 09 Python
Python cookbook(数据结构与算法)根据字段将记录分组操作示例
Mar 19 Python
详解Python对JSON中的特殊类型进行Encoder
Jul 15 Python
对Django外键关系的描述
Jul 26 Python
django框架面向对象ORM模型继承用法实例分析
Jul 29 Python
python或C++读取指定文件夹下的所有图片
Aug 31 Python
wxpython绘制圆角窗体
Nov 18 Python
python json.dumps() json.dump()的区别详解
Jul 14 Python
Python绘画好看的星空图
Mar 17 Python
使用Python开发冰球小游戏
详解Python中的for循环
Python采集壁纸并实现炫轮播
Apr 30 #Python
Python循环之while无限迭代
如何Python使用re模块实现okenizer
Apr 30 #Python
如何使用python包中的sched事件调度器
Apr 30 #Python
详解OpenCV获取高动态范围(HDR)成像
You might like
php操作xml
2013/10/27 PHP
详解在YII2框架中使用UEditor编辑器发布文章
2018/11/02 PHP
如何让动态插入的javascript脚本代码跑起来。
2007/01/09 Javascript
javascript中使用replaceAll()函数实现字符替换的方法
2010/12/25 Javascript
关于JS控制代码暂停的实现方法分享
2012/10/11 Javascript
JavaScript 处理Iframe自适应高度(同或不同域名下)
2013/03/29 Javascript
超简单JS二级、多级联动的简单实例
2014/02/18 Javascript
原生javascript实现图片按钮切换
2015/01/12 Javascript
JavaScript过滤字符串中的中文与空格方法汇总
2016/03/07 Javascript
webpack学习--webpack经典7分钟入门教程
2017/06/28 Javascript
详解如何用模块化的方式写vuejs
2017/12/16 Javascript
jQuery实现弹窗下底部页面禁止滑动效果
2017/12/19 jQuery
Vuex 入门教程
2018/01/10 Javascript
Node.js搭建WEB服务器的示例代码
2018/08/15 Javascript
vue中的自定义分页插件组件的示例
2018/08/18 Javascript
JS/HTML5游戏常用算法之追踪算法实例详解
2018/12/12 Javascript
前端js中的事件循环eventloop机制详解
2019/05/15 Javascript
Vue组件之高德地图地址选择功能的实例代码
2019/06/21 Javascript
jQuery实现异步上传一个或多个文件
2020/08/17 jQuery
Python中的进程分支fork和exec详解
2015/04/11 Python
Python实现完整的事务操作示例
2017/06/20 Python
用python脚本24小时刷浏览器的访问量方法
2018/12/07 Python
ubuntu上安装python的实例方法
2019/09/30 Python
详解mac python+selenium+Chrome 简单案例
2019/11/08 Python
PyQt5+Pycharm安装和配置图文教程详解
2020/03/24 Python
PyTorch 导数应用的使用教程
2020/08/31 Python
HTML5注册表单的自动聚焦与占位文本示例代码
2013/07/19 HTML / CSS
利用HTML5实现使用按钮控制背景音乐开关
2015/09/21 HTML / CSS
Boolean b = new Boolean(“abcde”); 会编译错误码
2013/11/27 面试题
餐厅楼面主管岗位职责范本
2014/02/16 职场文书
《蒙娜丽莎之约》教学反思
2014/02/27 职场文书
环保建议书200字
2014/05/14 职场文书
人力资源管理专业毕业生自荐书
2014/05/25 职场文书
暑期社会实践证明书
2014/11/17 职场文书
基于Redis zSet实现滑动窗口对短信进行防刷限流的问题
2022/02/12 Redis
python lambda 表达式形式分析
2022/04/03 Python