截至目前,我们可以通过面向对象思想,做一个属于自己的贪吃蛇游戏啦
尺寸相关
开发环境准备
安装依赖的库
pip install PyQt5
拷贝需要的资源
img.zip
将如上压缩包下载,并将里边的图片解压到工程根目录的img文件夹中。
准备Game游戏类
新建game.py
文件,拷贝如下内容到文件
from abc import abstractmethod
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
COLOR_GRAY = QtGui.QColor(128, 128, 128)
COLOR_RED = QtGui.QColor(255, 0, 0)
COLOR_GREEN = QtGui.QColor(0, 255, 0)
COLOR_BLUE = QtGui.QColor(0, 0, 255)
COLOR_WHITE = QtGui.QColor(255, 255, 255)
class Game(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Game")
self.timer = QtCore.QBasicTimer()
self.delay = 500
self.is_game_over = False
def set_fps(self, fps):
self.delay = 1000 // fps
if self.timer.isActive():
self.timer.stop()
self.timer.start(self.delay, self)
def timerEvent(self, event):
if self.is_game_over:
self.timer.stop()
else:
self.on_time_event(event)
self.repaint()
@abstractmethod
def on_time_event(self):
pass
@abstractmethod
def draw_content(self, qp: QtGui.QPainter):
pass
def paintEvent(self, event: QtGui.QPaintEvent):
qp = QtGui.QPainter()
qp.begin(self)
self.draw_content(qp)
qp.end()
def keyPressEvent(self, event: QtGui.QKeyEvent):
if event.key() == QtCore.Qt.Key_Escape:
self.close()
return
elif event.key() == QtCore.Qt.Key_Space:
if self.is_game_over:
self.start_game()
return
super().keyPressEvent(event)
def start_game(self):
self.is_game_over = False
self.timer.start(self.delay, self)
@classmethod
def start(cls):
app = QtWidgets.QApplication(sys.argv)
# 创建cls的对象
game = cls()
game.start_game()
game.show()
# while (1)
sys.exit(app.exec_())
继承Game游戏类(模板)
"""
继承了Game的空类
"""
from game import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
SCREEN_WIDTH = 640
SCREEN_HEIGHT = 480
class SnakeGame(Game):
def __init__(self):
super().__init__()
# 设置标题
self.setWindowTitle("SnakeGame")
# 设置窗口尺寸, 并禁止拖拽修改窗口尺寸
self.setFixedSize(SCREEN_WIDTH, SCREEN_HEIGHT)
# 设置窗口图标
self.setWindowIcon(QIcon("img/icon.png"))
def keyPressEvent(self, event: QKeyEvent):
"""处理按键事件
"""
super().keyPressEvent(event)
def on_time_event(self, event):
"""处理每帧时间事件
"""
pass
def draw_content(self, qp: QPainter):
"""绘制界面内容
:param qp: 画笔
"""
pass
def start_game(self):
"""游戏开始前的初始化
"""
super().start_game()
if __name__ == '__main__':
SnakeGame.start()
编写游戏逻辑
完整代码逻辑如下:
import random
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from game import *
SCREEN_WIDTH = 640
SCREEN_HEIGHT = 480
BLOCK_SIZE = 20
# 记录了每个方向对应的蛇头移动距离
MOVE_DICT = {
Qt.Key_Left: (-1, 0),
Qt.Key_Right: (1, 0),
Qt.Key_Up: (0, -1),
Qt.Key_Down: (0, 1)
}
# 记录了每个方向对应的蛇头图片旋转角度
DIRECTION_HEAD_ANGLE = {
Qt.Key_Left: 90,
Qt.Key_Right: 270,
Qt.Key_Up: 180,
Qt.Key_Down: 0
}
class Snake:
def __init__(self) -> None:
# 当前蛇头运动方向
self.direction = Qt.Key_Right
# 根据按键获取的最新蛇头运动方向
self.direction_temp = None
# 加载并缩放蛇头
self.head_img: QImage = QImage("img/head-red.png").scaled(BLOCK_SIZE, BLOCK_SIZE)
# 初始化蛇
self.snake_body = [
[3 * BLOCK_SIZE, 3 * BLOCK_SIZE],
]
# 生长2节
self.score = 0
self.grow()
self.grow()
def handle_key_event(self, event: QKeyEvent) -> bool:
"""判定按钮是否是方向的按键
并且判定能否改变到此方向
:param event: 按键事件
:return: True 能转向
"""
key = event.key()
LR = Qt.Key_Left , Qt.Key_Right
UD = Qt.Key_Up, Qt.Key_Down
if key not in LR and key not in UD:
return False
if key in LR and self.direction in LR:
return False
if key in UD and self.direction in UD:
return False
self.direction_temp = key
return True
def draw(self, qp: QPainter):
"""绘制蛇头蛇身
:param qp: 画笔
"""
qp.setBrush(COLOR_BLUE)
for node in self.snake_body[1:]:
# 绘制圆角矩形
# qp.drawRect(node[0], node[1], BLOCK_SIZE, BLOCK_SIZE)
qp.drawRoundedRect(node[0], node[1], BLOCK_SIZE, BLOCK_SIZE, 5, 5)
qp.setBrush(QColor(0, 255, 0))
head = self.snake_body[0]
# qp.drawRect(self.snake_body[0][0], head[1], BLOCK_SIZE, BLOCK_SIZE)
# 根据direction修改head_img旋转方向
rotated_head = self.head_img.transformed(QTransform().rotate(DIRECTION_HEAD_ANGLE[self.direction]))
qp.drawImage(head[0], head[1], rotated_head)
def move(self):
"""移动一格
"""
if self.direction_temp:
self.direction = self.direction_temp
self.direction_temp = None
# 修改头部坐标
new_head = self.snake_body[0][:]
new_move = MOVE_DICT[self.direction]
new_head[0] += new_move[0] * BLOCK_SIZE
new_head[1] += new_move[1] * BLOCK_SIZE
self.snake_body.insert(0, new_head)
# 删除尾部坐标
self.snake_body.pop()
def grow(self):
# 取出尾部坐标, 并复制一份
new_tail = self.snake_body[-1][:]
# 插入到尾部
self.snake_body.append(new_tail)
class Apple:
def __init__(self, x, y) -> None:
self.node = [x * BLOCK_SIZE, y * BLOCK_SIZE]
def draw(self, qp: QPainter):
qp.setBrush(QColor(255, 0, 0))
qp.drawRect(self.node[0], self.node[1], BLOCK_SIZE, BLOCK_SIZE)
class SnakeGame(Game):
def __init__(self):
super().__init__()
# 设置窗口尺寸, 并禁止拖拽修改窗口尺寸
self.setFixedSize(SCREEN_WIDTH, SCREEN_HEIGHT)
# 设置窗口标题
self.setWindowTitle("Snake Game")
# 图标
self.setWindowIcon(QIcon("img/icon.png"))
# 加载背景图, (缩放)
self.background_img = QImage("img/bg.png").scaled(SCREEN_WIDTH, SCREEN_HEIGHT)
# 设置fps,每秒5帧, 1000ms / 5 = 200ms
self.set_fps(5)
self.snake = None
self.apple = None
# 构建地图格点的二维数组
self.map = []
for i in range(SCREEN_WIDTH // BLOCK_SIZE):
for j in range(SCREEN_HEIGHT // BLOCK_SIZE):
self.map.append([i, j])
def keyPressEvent(self, event: QKeyEvent):
if self.snake.handle_key_event(event):
return
super().keyPressEvent(event)
def on_time_event(self, event: QTimerEvent):
"""处理每帧时间事件
:param event: 事件
"""
self.snake.move()
self.check_collision()
def draw_content(self, qp: QPainter):
# 绘制背景图
qp.drawImage(0, 0, self.background_img)
# 绘制网格线,间隔为BLOCK_SIZE
# 设置画笔为灰色
qp.setPen(COLOR_GRAY)
# 绘制横线.
# (0, 0) -> (SCREEN_WIDTH, 0)
# (0,20) -> (SCREEN_WIDTH, 20)
for y in range(0, SCREEN_HEIGHT, BLOCK_SIZE):
qp.drawLine(0, y, SCREEN_WIDTH, y)
# 绘制纵线
for x in range(0, SCREEN_WIDTH, BLOCK_SIZE):
qp.drawLine(x, 0, x, SCREEN_HEIGHT)
self.snake.draw(qp)
self.apple.draw(qp)
# 绘制文字
qp.setPen(COLOR_WHITE)
# 设置字号22, 字体 Arial
qp.setFont(QFont("Arial", 18))
# 左上角得分 Score: xxx
qp.drawText(20, 30, f"Score: {self.snake.score}")
if self.is_game_over:
# 设置字号20, 字体 Microsoft YaHei
qp.setFont(QFont("Microsoft YaHei", 20, weight=QFont.Bold))
# Game Over
qp.drawText(SCREEN_WIDTH // 2 - 100, SCREEN_HEIGHT // 2, "游戏结束")
qp.setFont(QFont("Microsoft YaHei", 14))
qp.drawText(SCREEN_WIDTH // 2 - 100, SCREEN_HEIGHT // 2 + 40, "得分:" + str(self.snake.score))
qp.drawText(SCREEN_WIDTH // 2 - 100, SCREEN_HEIGHT // 2 + 70, "按空格重新开始")
def check_collision(self):
"""碰撞检测
"""
snake_head = self.snake.snake_body[0]
if snake_head == self.apple.node:
print("吃到苹果")
self.generate_new_apple()
self.snake.grow()
self.snake.score += 1
# 如果蛇身占满地图,游戏结束
if len(self.snake.snake_body) >= len(self.map):
print("游戏胜利")
self.is_game_over = True
# 碰到自己
if snake_head in self.snake.snake_body[1:]:
self.is_game_over = True
print("碰到自己")
# 碰到墙
head_x, head_y = snake_head
if head_x < 0 or head_x >= SCREEN_WIDTH or head_y < 0 or head_y >= SCREEN_HEIGHT:
self.is_game_over = True
print("碰到墙")
def generate_new_apple(self):
"""生成新的苹果坐标
"""
# 复制一份self.map为新的temp_map
temp_map = self.map[:]
# 将 temp_map 中snake的坐标删除
for node in self.snake.snake_body:
node_pos = [node[0] // BLOCK_SIZE, node[1] // BLOCK_SIZE]
if node_pos in temp_map:
temp_map.remove(node_pos)
if len(temp_map) == 0:
return
# 生成新的苹果坐标
new_pos = random.choice(temp_map)
print("new_pos: ", new_pos)
self.apple = Apple(new_pos[0], new_pos[1])
def start_game(self):
self.snake = Snake()
# self.apple = Apple(2, 2)
self.generate_new_apple()
super().start_game()
if __name__ == "__main__":
SnakeGame.start()