截至目前,我们可以通过面向对象思想,做一个属于自己的贪吃蛇游戏啦
image.png

尺寸相关

窗口尺寸:640 x 480px
蛇身块大小:20px

开发环境准备

安装依赖的库

  1. pip install PyQt5

拷贝需要的资源

img.zip
将如上压缩包下载,并将里边的图片解压到工程根目录的img文件夹中。

准备Game游戏类

新建game.py文件,拷贝如下内容到文件

  1. from abc import abstractmethod
  2. from PyQt5 import QtCore, QtGui, QtWidgets
  3. import sys
  4. COLOR_GRAY = QtGui.QColor(128, 128, 128)
  5. COLOR_RED = QtGui.QColor(255, 0, 0)
  6. COLOR_GREEN = QtGui.QColor(0, 255, 0)
  7. COLOR_BLUE = QtGui.QColor(0, 0, 255)
  8. COLOR_WHITE = QtGui.QColor(255, 255, 255)
  9. class Game(QtWidgets.QWidget):
  10. def __init__(self):
  11. super().__init__()
  12. self.setWindowTitle("Game")
  13. self.timer = QtCore.QBasicTimer()
  14. self.delay = 500
  15. self.is_game_over = False
  16. def set_fps(self, fps):
  17. self.delay = 1000 // fps
  18. if self.timer.isActive():
  19. self.timer.stop()
  20. self.timer.start(self.delay, self)
  21. def timerEvent(self, event):
  22. if self.is_game_over:
  23. self.timer.stop()
  24. else:
  25. self.on_time_event(event)
  26. self.repaint()
  27. @abstractmethod
  28. def on_time_event(self):
  29. pass
  30. @abstractmethod
  31. def draw_content(self, qp: QtGui.QPainter):
  32. pass
  33. def paintEvent(self, event: QtGui.QPaintEvent):
  34. qp = QtGui.QPainter()
  35. qp.begin(self)
  36. self.draw_content(qp)
  37. qp.end()
  38. def keyPressEvent(self, event: QtGui.QKeyEvent):
  39. if event.key() == QtCore.Qt.Key_Escape:
  40. self.close()
  41. return
  42. elif event.key() == QtCore.Qt.Key_Space:
  43. if self.is_game_over:
  44. self.start_game()
  45. return
  46. super().keyPressEvent(event)
  47. def start_game(self):
  48. self.is_game_over = False
  49. self.timer.start(self.delay, self)
  50. @classmethod
  51. def start(cls):
  52. app = QtWidgets.QApplication(sys.argv)
  53. # 创建cls的对象
  54. game = cls()
  55. game.start_game()
  56. game.show()
  57. # while (1)
  58. sys.exit(app.exec_())

继承Game游戏类(模板)

  1. """
  2. 继承了Game的空类
  3. """
  4. from game import *
  5. from PyQt5.QtCore import *
  6. from PyQt5.QtGui import *
  7. from PyQt5.QtWidgets import *
  8. SCREEN_WIDTH = 640
  9. SCREEN_HEIGHT = 480
  10. class SnakeGame(Game):
  11. def __init__(self):
  12. super().__init__()
  13. # 设置标题
  14. self.setWindowTitle("SnakeGame")
  15. # 设置窗口尺寸, 并禁止拖拽修改窗口尺寸
  16. self.setFixedSize(SCREEN_WIDTH, SCREEN_HEIGHT)
  17. # 设置窗口图标
  18. self.setWindowIcon(QIcon("img/icon.png"))
  19. def keyPressEvent(self, event: QKeyEvent):
  20. """处理按键事件
  21. """
  22. super().keyPressEvent(event)
  23. def on_time_event(self, event):
  24. """处理每帧时间事件
  25. """
  26. pass
  27. def draw_content(self, qp: QPainter):
  28. """绘制界面内容
  29. :param qp: 画笔
  30. """
  31. pass
  32. def start_game(self):
  33. """游戏开始前的初始化
  34. """
  35. super().start_game()
  36. if __name__ == '__main__':
  37. SnakeGame.start()

image.png

编写游戏逻辑

完整代码逻辑如下:

  1. import random
  2. from PyQt5.QtCore import *
  3. from PyQt5.QtGui import *
  4. from PyQt5.QtWidgets import *
  5. from game import *
  6. SCREEN_WIDTH = 640
  7. SCREEN_HEIGHT = 480
  8. BLOCK_SIZE = 20
  9. # 记录了每个方向对应的蛇头移动距离
  10. MOVE_DICT = {
  11. Qt.Key_Left: (-1, 0),
  12. Qt.Key_Right: (1, 0),
  13. Qt.Key_Up: (0, -1),
  14. Qt.Key_Down: (0, 1)
  15. }
  16. # 记录了每个方向对应的蛇头图片旋转角度
  17. DIRECTION_HEAD_ANGLE = {
  18. Qt.Key_Left: 90,
  19. Qt.Key_Right: 270,
  20. Qt.Key_Up: 180,
  21. Qt.Key_Down: 0
  22. }
  23. class Snake:
  24. def __init__(self) -> None:
  25. # 当前蛇头运动方向
  26. self.direction = Qt.Key_Right
  27. # 根据按键获取的最新蛇头运动方向
  28. self.direction_temp = None
  29. # 加载并缩放蛇头
  30. self.head_img: QImage = QImage("img/head-red.png").scaled(BLOCK_SIZE, BLOCK_SIZE)
  31. # 初始化蛇
  32. self.snake_body = [
  33. [3 * BLOCK_SIZE, 3 * BLOCK_SIZE],
  34. ]
  35. # 生长2节
  36. self.score = 0
  37. self.grow()
  38. self.grow()
  39. def handle_key_event(self, event: QKeyEvent) -> bool:
  40. """判定按钮是否是方向的按键
  41. 并且判定能否改变到此方向
  42. :param event: 按键事件
  43. :return: True 能转向
  44. """
  45. key = event.key()
  46. LR = Qt.Key_Left , Qt.Key_Right
  47. UD = Qt.Key_Up, Qt.Key_Down
  48. if key not in LR and key not in UD:
  49. return False
  50. if key in LR and self.direction in LR:
  51. return False
  52. if key in UD and self.direction in UD:
  53. return False
  54. self.direction_temp = key
  55. return True
  56. def draw(self, qp: QPainter):
  57. """绘制蛇头蛇身
  58. :param qp: 画笔
  59. """
  60. qp.setBrush(COLOR_BLUE)
  61. for node in self.snake_body[1:]:
  62. # 绘制圆角矩形
  63. # qp.drawRect(node[0], node[1], BLOCK_SIZE, BLOCK_SIZE)
  64. qp.drawRoundedRect(node[0], node[1], BLOCK_SIZE, BLOCK_SIZE, 5, 5)
  65. qp.setBrush(QColor(0, 255, 0))
  66. head = self.snake_body[0]
  67. # qp.drawRect(self.snake_body[0][0], head[1], BLOCK_SIZE, BLOCK_SIZE)
  68. # 根据direction修改head_img旋转方向
  69. rotated_head = self.head_img.transformed(QTransform().rotate(DIRECTION_HEAD_ANGLE[self.direction]))
  70. qp.drawImage(head[0], head[1], rotated_head)
  71. def move(self):
  72. """移动一格
  73. """
  74. if self.direction_temp:
  75. self.direction = self.direction_temp
  76. self.direction_temp = None
  77. # 修改头部坐标
  78. new_head = self.snake_body[0][:]
  79. new_move = MOVE_DICT[self.direction]
  80. new_head[0] += new_move[0] * BLOCK_SIZE
  81. new_head[1] += new_move[1] * BLOCK_SIZE
  82. self.snake_body.insert(0, new_head)
  83. # 删除尾部坐标
  84. self.snake_body.pop()
  85. def grow(self):
  86. # 取出尾部坐标, 并复制一份
  87. new_tail = self.snake_body[-1][:]
  88. # 插入到尾部
  89. self.snake_body.append(new_tail)
  90. class Apple:
  91. def __init__(self, x, y) -> None:
  92. self.node = [x * BLOCK_SIZE, y * BLOCK_SIZE]
  93. def draw(self, qp: QPainter):
  94. qp.setBrush(QColor(255, 0, 0))
  95. qp.drawRect(self.node[0], self.node[1], BLOCK_SIZE, BLOCK_SIZE)
  96. class SnakeGame(Game):
  97. def __init__(self):
  98. super().__init__()
  99. # 设置窗口尺寸, 并禁止拖拽修改窗口尺寸
  100. self.setFixedSize(SCREEN_WIDTH, SCREEN_HEIGHT)
  101. # 设置窗口标题
  102. self.setWindowTitle("Snake Game")
  103. # 图标
  104. self.setWindowIcon(QIcon("img/icon.png"))
  105. # 加载背景图, (缩放)
  106. self.background_img = QImage("img/bg.png").scaled(SCREEN_WIDTH, SCREEN_HEIGHT)
  107. # 设置fps,每秒5帧, 1000ms / 5 = 200ms
  108. self.set_fps(5)
  109. self.snake = None
  110. self.apple = None
  111. # 构建地图格点的二维数组
  112. self.map = []
  113. for i in range(SCREEN_WIDTH // BLOCK_SIZE):
  114. for j in range(SCREEN_HEIGHT // BLOCK_SIZE):
  115. self.map.append([i, j])
  116. def keyPressEvent(self, event: QKeyEvent):
  117. if self.snake.handle_key_event(event):
  118. return
  119. super().keyPressEvent(event)
  120. def on_time_event(self, event: QTimerEvent):
  121. """处理每帧时间事件
  122. :param event: 事件
  123. """
  124. self.snake.move()
  125. self.check_collision()
  126. def draw_content(self, qp: QPainter):
  127. # 绘制背景图
  128. qp.drawImage(0, 0, self.background_img)
  129. # 绘制网格线,间隔为BLOCK_SIZE
  130. # 设置画笔为灰色
  131. qp.setPen(COLOR_GRAY)
  132. # 绘制横线.
  133. # (0, 0) -> (SCREEN_WIDTH, 0)
  134. # (0,20) -> (SCREEN_WIDTH, 20)
  135. for y in range(0, SCREEN_HEIGHT, BLOCK_SIZE):
  136. qp.drawLine(0, y, SCREEN_WIDTH, y)
  137. # 绘制纵线
  138. for x in range(0, SCREEN_WIDTH, BLOCK_SIZE):
  139. qp.drawLine(x, 0, x, SCREEN_HEIGHT)
  140. self.snake.draw(qp)
  141. self.apple.draw(qp)
  142. # 绘制文字
  143. qp.setPen(COLOR_WHITE)
  144. # 设置字号22, 字体 Arial
  145. qp.setFont(QFont("Arial", 18))
  146. # 左上角得分 Score: xxx
  147. qp.drawText(20, 30, f"Score: {self.snake.score}")
  148. if self.is_game_over:
  149. # 设置字号20, 字体 Microsoft YaHei
  150. qp.setFont(QFont("Microsoft YaHei", 20, weight=QFont.Bold))
  151. # Game Over
  152. qp.drawText(SCREEN_WIDTH // 2 - 100, SCREEN_HEIGHT // 2, "游戏结束")
  153. qp.setFont(QFont("Microsoft YaHei", 14))
  154. qp.drawText(SCREEN_WIDTH // 2 - 100, SCREEN_HEIGHT // 2 + 40, "得分:" + str(self.snake.score))
  155. qp.drawText(SCREEN_WIDTH // 2 - 100, SCREEN_HEIGHT // 2 + 70, "按空格重新开始")
  156. def check_collision(self):
  157. """碰撞检测
  158. """
  159. snake_head = self.snake.snake_body[0]
  160. if snake_head == self.apple.node:
  161. print("吃到苹果")
  162. self.generate_new_apple()
  163. self.snake.grow()
  164. self.snake.score += 1
  165. # 如果蛇身占满地图,游戏结束
  166. if len(self.snake.snake_body) >= len(self.map):
  167. print("游戏胜利")
  168. self.is_game_over = True
  169. # 碰到自己
  170. if snake_head in self.snake.snake_body[1:]:
  171. self.is_game_over = True
  172. print("碰到自己")
  173. # 碰到墙
  174. head_x, head_y = snake_head
  175. if head_x < 0 or head_x >= SCREEN_WIDTH or head_y < 0 or head_y >= SCREEN_HEIGHT:
  176. self.is_game_over = True
  177. print("碰到墙")
  178. def generate_new_apple(self):
  179. """生成新的苹果坐标
  180. """
  181. # 复制一份self.map为新的temp_map
  182. temp_map = self.map[:]
  183. # 将 temp_map 中snake的坐标删除
  184. for node in self.snake.snake_body:
  185. node_pos = [node[0] // BLOCK_SIZE, node[1] // BLOCK_SIZE]
  186. if node_pos in temp_map:
  187. temp_map.remove(node_pos)
  188. if len(temp_map) == 0:
  189. return
  190. # 生成新的苹果坐标
  191. new_pos = random.choice(temp_map)
  192. print("new_pos: ", new_pos)
  193. self.apple = Apple(new_pos[0], new_pos[1])
  194. def start_game(self):
  195. self.snake = Snake()
  196. # self.apple = Apple(2, 2)
  197. self.generate_new_apple()
  198. super().start_game()
  199. if __name__ == "__main__":
  200. SnakeGame.start()