Python 基础知识我们已经学完了,接下来我们做点小练习,巩固掌握的 Python 知识,做点入门小游戏既能应用已经学习的 Python 基础知识,又能有些乐趣。哈哈 ,入门小游戏 可以有 弹球、贪吃蛇、坦克大战、飞机大战等等。
这一篇我们从贪吃蛇入手吧,Python 的图形库常用的有 pygame、arcade,pygame 下载量比较高,用户量大,教程比较多的库,Arcade 则更倾向于现代风格面向对象的 api 设计,我们不讨论他们的优劣,对于我们使用来说没有太大区别,我们都用,我们接下来使用 pygame 和 arcade 、还有 开发各平台游戏的 当红炸子鸡 Cocos 分别来实现。
我们先抛开代码的范畴,了解下 贪吃蛇 这个游戏的实现思路。需要解决的问题:
1. 蛇的移动 2. 蛇移动的快慢 3. 食物的种类和效果 4. 食物的随机位置不要和蛇重叠 5. 提示 UI 的绘制

如图贪吃蛇的移动方向向右,那么贪吃蛇移动的时候,A 箭头对应的点将成为新的舌头,B 箭头对应的点将从贪吃蛇的数据结构中删除,这样一添一删后,重新渲染蛇的位置,就完成了蛇向右移动一格子。
贪吃蛇我们使用列表来存储,移动就是在当前的移动方向上取下一个格子,加入列表的头部,如果没吃到食物,就去掉尾部,如果吃到食物,那么只添加蛇头,这样就完成了贪吃蛇吃食物和移动的逻辑。蛇身体中间的位置是不用动的,只操作蛇头和蛇尾。
我们把背景定义成一定数量的格子,格子的大小是固定的,一个格子的大小就是贪吃蛇的一节身体,这样贪吃蛇每次移动一格,通过多久移动一格来定义速度的快慢,时间短移动一格,那么贪吃蛇的移动速度就会快,这样定义的好处是:既和帧率无关,又不会出现每次移动半格吃不到食物的情况。
这两个问题搞好了,那么其他食物种类啊,食物生成位置不和贪吃蛇重叠这些问题就迎刃而解了。
我们接下来先试用 pygame 来实现。下一篇使用 arcade 来实现一样的效果。
classGlobalConfig(): CELL_SIZE = 15# 蛇和食物的基础单元尺寸 INITIAL_SNAKE_LENGTH = 5# 蛇初始长度# Colors (R, G, B) BLACK = pygame.Color(0, 0, 0) WHITE = pygame.Color(255, 255, 255) RED = pygame.Color(255, 0, 0) ORANGE = pygame.Color(255, 165, 0) GRAY = pygame.Color(128, 128, 128)def __init__(self):# 难度设定# Easy -> 10# Medium -> 25# Hard -> 40# Harder -> 60# Impossible-> 120self.difficulty: float = 1 / 10self.speed: float = self.difficultyself.accumulator: float = 0# 食物概率配置self.FOOD_NORMAL = 40# 普通食物概率40%self.FOOD_ACCEL = 30# 加速食物概率30%self.FOOD_HYPER = 30# 超级加速食物概率30%self.x_boundary = (0, WINDOW_WIDTH // GlobalConfig.CELL_SIZE)self.y_boundary = (2, WINDOW_HEIGHT // GlobalConfig.CELL_SIZE)self.line_width = 1self.show_debug_info = Falseself.snake_color = GlobalConfig.WHITEself.snake_food_color = GlobalConfig.WHITEself.best_score = 0self.game_window = Noneself.game_manager: GameManager = None定义贪吃蛇的基础数据, 格子大小 15 个像素,蛇初始长度 5,使用 pygame 的颜色定义几个贪吃蛇的颜色。还有每种食物的生成概率。
实现一下 pygame 的游戏循环和初始化等操作
classGameManager():def __init__(self):self.game_view = GameView()self.over_view = GameOverView()self.running_view = self.game_viewself.last_update_time = time.time() game_config.game_manager = selfdef init_game(self): pygame.init() pygame.display.set_caption(WINDOW_TITLE) game_config.game_window = pygame.display.set_mode( (WINDOW_WIDTH, WINDOW_HEIGHT)) @classmethoddef draw_text(cls, text, pos_x, pos_y, color, font_size=30): show_font = pygame.font.SysFont('SimHei', font_size) game_over_surface = show_font.render(text, True, color) game_over_rect = game_over_surface.get_rect() game_over_rect.midtop = (pos_x, pos_y) game_config.game_window.blit(game_over_surface, game_over_rect)def run(self):# Main logicwhile True:for event in pygame.event.get():if event.type == pygame.QUIT: pygame.quit()# sys.exit()elif event.type == pygame.KEYDOWN:self.running_view.on_key_press(event.key)elif event.type == pygame.MOUSEBUTTONUP:self.running_view.on_mouse_press() cur_time = time.time()self.running_view.on_fixed_update(cur_time - self.last_update_time)self.last_update_time = cur_timeself.running_view.on_draw() pygame.display.update()def change_to_gameview(self):self.running_view = self.game_viewself.game_view.init_game_elements()def change_to_overview(self):self.running_view = self.over_view先定义一个列表来存储贪吃蛇的关节,初始位置和移动方向class Snake():def __init__(self):self.segments: list = []self.reset()def reset(self):"""重置蛇的位置和状态""" start_y = game_config.y_boundary[1] // 2self.segments = []# 生成初始蛇身(水平向右)for i in range(game_config.INITIAL_SNAKE_LENGTH):self.segments.insert(0, (i, start_y))# 重置蛇的初始状态self.direction = pygame.K_RIGHT在 update 里根据移动方向,来生成一个贪吃蛇的头部插入到列表的头部,定义 remove_tail 来移除尾部。check_self_collision 来检测是否碰撞到贪吃蛇的身体。
defupdate(self, delta_time: float):"""更新蛇的位置,根据移动方向""" head_x, head_y = self.segments[0]match self.direction:case pygame.K_UP: head_y -= 1case pygame.K_DOWN: head_y += 1case pygame.K_LEFT: head_x -= 1case pygame.K_RIGHT: head_x += 1# 插入新的头部self.segments.insert(0, (head_x, head_y))def remove_tail(self):# 根据吃到食物的情况,决定是否去除尾部self.segments.pop()def check_self_collision(self) -> bool:"""检测是否碰撞自身"""# 遍历除头部外的身体段for segment inself.segments[1:]:# 精确碰撞:头部与身体段完全重叠if (segment == self.segments[0]):returnTruereturnFalse定义 check_collision 来检测是否自己碰撞到自己,还有是否碰撞到边缘,在 draw 函数中使用矩形绘制当前的蛇的身体位置。
defcheck_boundary_collision(self) -> bool: head_x, head_y = self.segments[0]# 边界判定:头部超出屏幕范围if (head_x < game_config.x_boundary[0] or head_x >= game_config.x_boundary[1] or head_y < game_config.y_boundary[0] or head_y >= game_config.y_boundary[1] ):returnTruereturnFalsedef check_collision(self) -> bool:returnself.check_boundary_collision() orself.check_self_collision()def draw(self):"""绘制蛇"""for segment inself.segments: pygame.draw.rect( game_config.game_window, game_config.snake_color, pygame.Rect(segment[0] * game_config.CELL_SIZE + game_config.line_width, segment[1] * game_config.CELL_SIZE + game_config.line_width, game_config.CELL_SIZE - game_config.line_width, game_config.CELL_SIZE - game_config.line_width ) )最后是 检测按键状态,以及防止 180 度转向:
defon_key_press(self, key: int):# 改变蛇的方向(防止180度转弯)if key == pygame.K_UP andself.direction != pygame.K_DOWN:self.direction = pygame.K_UPelif key == pygame.K_DOWN andself.direction != pygame.K_UP:self.direction = pygame.K_DOWNelif key == pygame.K_LEFT andself.direction != pygame.K_RIGHT:self.direction = pygame.K_LEFTelif key == pygame.K_RIGHT andself.direction != pygame.K_LEFT:self.direction = pygame.K_RIGHT我们定义一个食物的基类,来处理食物的概率生成,还有位置合法性校验的逻辑。然后定义 effect 函数来实现吃到食物后的效果加成,然后在不同的子类中分别实现,贪吃蛇吃到食物后的效果加成逻辑就可以通过多态来实现不同的效果。
classSnakeFood():def __init__(self):self.x: int = 0self.y: int = 0self.reset()self.reset_food_color()def reset(self):self.x = random.randint( game_config.x_boundary[0], game_config.x_boundary[1] - 1)self.y = random.randint( game_config.y_boundary[0], game_config.y_boundary[1] - 1)def reset_food_color(self):passdef validate_position(self, position_list: list):while True:if (self.x, self.y) notin position_list:breakelse:self.reset()def draw(self): pygame.draw.rect( game_config.game_window, game_config.snake_food_color, pygame.Rect(self.x * game_config.CELL_SIZE + game_config.line_width,self.y * game_config.CELL_SIZE + game_config.line_width, game_config.CELL_SIZE - game_config.line_width, game_config.CELL_SIZE - game_config.line_width ) )def effect(self) -> str:return"" @classmethoddef random_food(cls):""" 按概率随机生成食物实例 :return: SnakeFoodNormal / SnakeFoodAccelerated 实例 """# 1. 计算概率总和(容错:避免配置错误) total_prob = game_config.FOOD_NORMAL + \ game_config.FOOD_ACCEL + game_config.FOOD_HYPERif total_prob <= 0: total_prob = 100# 兜底:默认100%普通食物 game_config.FOOD_NORMAL = 100 game_config.FOOD_ACCEL = 0 game_config.FOOD_HYPER = 0# 2. 生成随机数,按概率区间判断 rand_num = random.randint(1, total_prob)# 3. 普通食物区间:1 ~ FOOD_NORMAL_if rand_num <= game_config.FOOD_NORMAL:return SnakeFoodNormal()# 4. 加速食物区间:FOOD_NORMAL ~ FOOD_ACCELelif rand_num <= game_config.FOOD_NORMAL + game_config.FOOD_ACCEL:return SnakeFoodAccelerated()else:return SnakeFoodHyper()然后用 3 个食物的类型分别来实现不同的食物类型。
classSnakeFoodNormal(SnakeFood):def reset_food_color(self): game_config.snake_food_color = game_config.WHITEdef effect(self) -> str: game_config.speed = game_config.difficulty game_config.snake_color = game_config.WHITEreturn"normal"class SnakeFoodAccelerated(SnakeFood):def reset_food_color(self): game_config.snake_food_color = game_config.ORANGEdef effect(self) -> str: game_config.speed = game_config.difficulty * 0.7 game_config.snake_color = game_config.ORANGEreturn"accelerated"class SnakeFoodHyper(SnakeFood):def reset_food_color(self): game_config.snake_food_color = game_config.REDdef effect(self) -> str: game_config.speed = game_config.difficulty * 0.5 game_config.snake_color = game_config.REDreturn"hyper"游戏分数记录和游戏界面,游戏结束界面等相关的功能:游戏分数记录功能,使用 GameManager 封装的 pygame 的 draw_text 函数
classGameScore():def __init__(self):self.score = 0def add_point(self):"""增加分数"""self.score += 1ifself.score > game_config.best_score: game_config.best_score = self.scoredef reset(self):"""重置分数"""self.score = 0def draw(self):"""绘制分数""" GameManager.draw_text( text=f"分数: {self.score}", pos_x=WINDOW_WIDTH - 200, pos_y=game_config.CELL_SIZE * 0.7, color=game_config.WHITE, font_size=15 ) GameManager.draw_text( text=f"最佳分数: {game_config.best_score}", pos_x=200, pos_y=game_config.CELL_SIZE * 0.7, color=game_config.WHITE, font_size=15 )然后定义一个游戏界面的基类,在整个游戏流程中主循环不用关心具体在哪个游戏界面
classBaseView():def on_update(self, delta_time: float) -> bool | None:passdef on_fixed_update(self, delta_time: float):passdef on_draw(self) -> bool | None:passdef on_key_press(self, key: int):passdef on_mouse_press(self):pass然后在 GameView 界面中实现游戏界面的功能,整个贪吃蛇游戏的功能部分都实现在这里
classGameView(BaseView):def __init__(self):self.init_game_elements()def on_fixed_update(self, delta_time: float): game_config.accumulator += delta_timeif game_config.accumulator < game_config.speed:returnelse: game_config.accumulator = 0# 1. 更新蛇的位置self.snake.update(delta_time)# 2. 检测蛇是否吃到食物if not self.check_food_collision():self.snake.remove_tail()else:self.game_score.add_point()self.snake_food.effect()self.snake_food = SnakeFood.random_food()self.snake_food.validate_position(self.snake.segments)self.debug_pos_info()# 3. 检测是否碰撞if self.snake.check_collision(): game_config.game_manager.change_to_overview()returndef on_draw(self) -> bool | None: game_config.game_window.fill(game_config.BLACK)self.draw_grid()self.snake.draw()self.snake_food.draw()self.game_score.draw()def draw_grid(self): x_boundary_left, x_boundary_right = game_config.x_boundary y_boundary_down, y_boundary_up = game_config.y_boundary# 竖向格子for x in range(x_boundary_left, x_boundary_right + 1): pygame.draw.line( game_config.game_window, GlobalConfig.GRAY, (x * game_config.CELL_SIZE, y_boundary_down * game_config.CELL_SIZE), (x * game_config.CELL_SIZE, y_boundary_up * game_config.CELL_SIZE), game_config.line_width)# 横向格子for y in range(y_boundary_down, y_boundary_up + 1): pygame.draw.line( game_config.game_window, GlobalConfig.GRAY, (x_boundary_left * game_config.CELL_SIZE, y * game_config.CELL_SIZE), (x_boundary_right * game_config.CELL_SIZE, y * game_config.CELL_SIZE), game_config.line_width)def on_key_press(self, key: int):"""处理键盘输入"""# 按R键重置游戏if key == pygame.K_r:self.init_game_elements()else:self.snake.on_key_press(key)def init_game_elements(self):self.snake = Snake()self.game_score = GameScore()self.snake_food = SnakeFood.random_food()self.snake_food.validate_position(self.snake.segments) game_config.speed = game_config.difficulty game_config.snake_color = game_config.WHITEself.game_over = Falsedef check_food_collision(self) -> bool: snake_head_pos = self.snake.segments[0] snake_food_pos = (self.snake_food.x, self.snake_food.y)if snake_head_pos == snake_food_pos:return Truereturn Falsedef debug_pos_info(self):if game_config.show_debug_info: snake_pos = " ".join([str(segment)for segment inself.snake.segments])print(f"snake pos is {snake_pos}")print(f"food pos is ({self.snake_food.x}, {self.snake_food.y})")最后定义一下游戏结束类,和 main 入口函数
classGameOverView(BaseView):def on_show_view(self):self.window.background_color = game_config.BLACKdef on_draw(self): game_config.game_window.fill(game_config.BLACK) GameManager.draw_text( text="游戏结束 - 鼠标点击任意位置或者Enter键重启", pos_x=WINDOW_WIDTH // 2, pos_y=WINDOW_HEIGHT // 2, color=game_config.WHITE, font_size=20 )def on_key_press(self, key: int):if key in (pygame.K_KP_ENTER, pygame.K_RETURN): game_config.game_manager.change_to_gameview()def on_mouse_press(self): game_config.game_manager.change_to_gameview()def main(): game_manager = GameManager() game_manager.init_game() game_manager.run()if __name__ == "__main__": main()贪吃蛇游戏没有任何游戏资源,只使用 pygame 的绘制矩形和直线等 api 来绘制,这里基本把所有用到的贪吃蛇游戏代码都贴出来了,原则上复制到一个文件里就可以运行。由于篇幅有限就不整体贴在一起了。如果实在动手出错,或者初学,那留言说明,一一答复吧。
下一篇我们使用 Arcade 实现完全一模一样的功能。
如果觉着本篇对您有点帮助,可以点一个在看和红心,是对我最大的鼓舞,如果对您有帮助或者篇中内容有纰漏,也请您留言告知。感谢!
<Python起源><Python之禅><Python中的变量并不是我们通常理解的变量><Python的基本数据类型><Python的循环结构><Python字符串的前生今世><Python复合数据类型之列表><Python字典从入门到精通><Python面向对象编程(OOP)全面学习与分析><Python模块化开发从入门到精通>
感谢您的订阅,关注和阅读!为您提供Python的入门和进阶笔记,对Python深入思考剖析,并一步步实践,一起学习进步。