前言
💡 本人为在读人工智能研一的学生,虽然本科也是学计算机出身,奈何学业不精且从事三四年非本专业相关工作,对很多代码语言包括知识都一知半解甚至没什么印象。研究生糊里糊涂过了半学期,想到年龄,行业压力以及规划目标,惊觉不能再摆下去,因此准备用博客记录学习历程,一是给自己形成系统的学习框架以便回顾,二来也变相敦促自己。因此将简单贪吃蛇游戏作为第一站吧。
初步准备--让贪吃蛇动起来吧
需求分析
贪吃蛇游戏大家都不陌生,这也是python练手项目之一,为了更好的进行需求分析和代码编写,我们来回顾一下贪吃蛇游戏的规则:
- 玩家操作贪吃蛇前进,尽可能收集食物取得高分;
- 贪吃蛇在获取食物后,身体长度会进一步加长,同时食物消失并随机生成新的食物;
- 贪吃蛇不小心撞向墙壁或自己时,游戏结束。
以下是贪吃蛇游戏的基本流程图:
在制作python简单游戏时,我们必不可少要借助一个第三方库—pygame:
Pygame是一个利用SDL库的游戏库, 是一组用来开发游戏软件的 Python 程序模块。SDL(Simple DirectMedia Layer)是一个跨平台库,支持访问计算机多媒体硬件(声音、视频、输入等),SDL非常强大,但美中不足的是它是基于 C 语言的。 PyGame是 SDL 库的 Python 包装器(wrapper),Pygame 在SDL库的基础上提供了各种接口,从而使用用户能够使用python语言创建各种各样的游戏或多媒体程序。
我们可以在终端中使用代码导入,也可以通过在Pycharm中配置package获得
pip install pygame
import pygame
import time, sys, random
import pygame
import random, sys, time
from pygame.locals import *
from pygame.math import Vector2
# 定义颜色块 RGB
green = pygame.Color(110, 150, 61)
orange = pygame.Color(255, 127, 80)
black = pygame.Color(0, 0, 0)
purple = pygame.Color(230, 230, 250)
grey = pygame.Color(105, 105, 105)
brown = pygame.Color(139, 129, 76)
blue = pygame.Color(70, 130, 180)
# pygame初始化
pygame.init()
cell_size = 20
cell_num = 40
score = 0
'''全局参数定义'''
# 定义窗口大小,标题,游戏界面内字体及大小
screen = pygame.display.set_mode((720, 480))
pygame.display.set_caption("Snake Game")
BASICFONT = pygame.font.SysFont("times new roman", 20)
# 定义变量来控制游戏速度
clock = pygame.time.Clock()
创建类和运行主体
从面向对象的角度来讲,本游戏有两个主要对象,蛇和食物,因此我们也要相应创建并绘制蛇类Snake()和食物类Food(),类中包含了其对应的属性和功能,pygame 中提供了一个draw模块用来绘制一些简单的图形状,在这里我们用pygame.draw.rect()** **绘制矩形,同时我们也要创建一个将蛇和食物的行为联系起来的主函数Main()
蛇
创建Snake类表示和绘制出贪吃蛇,蛇长占三格,每格长度为10, 用列表来存放“蛇身”的坐标
属性:
- 初始位置,即蛇头的位置
- 初始长度
- 前进方向
class Snake:
def __init__(self):
# 贪吃蛇的初始位置
self.head = Vector2(5, 10)
# 定义蛇的长度,占3个方格,每个方格长度为10
self.body = [Vector2(5, 10), Vector2(6, 10), Vector2(7, 10)]
#定义蛇移动方向
self.direction = pygame.K_RIGHT
def draw_snake(self):
for block in self.body:
x_pos = int(block.x * cell_size)
y_pos = int(block.y * cell_size)
block_rect = pygame.Rect(x_pos, y_pos, cell_size, cell_size)
pygame.draw.rect(screen, green, block_rect)
方法:
移动和增加蛇长
在游戏过程中,我们通过键盘控制贪吃蛇的移动,在这里我们设置用WASD或上下左右键来操作。同时,我们也要考虑到不允许蛇立刻向反方向移动的情况。 在这个模块最重要的是如何体现出移动的状态,我们可以将整个身体看成一个列表,头部为0,移动就是取身体上从第一个到倒数第二个为止的元素,即删除了最后一个元素, 然后用insert方法将新的元素插到第一个元素前面成为新的头部。
def move_snake(self):
#取整条蛇的第一个到倒数第二个部分
body_copy=self.body[:-1]
body_copy.insert(0,body_copy[0]+self.direction)
#更新身体位置
self.body=body_copy[:]
在上一步我们重点讲述了如何让蛇移动,即取除了尾部的全部身长,再在移动方向最头部增加一个元素块即可,这样蛇的位置发生改变而蛇长不变,若我们想增加蛇长只需将第一步改为取全部身长即可,具体方法如下:
def add_length(self):
#取整条蛇部分
body_copy=self.body[:]
body_copy.insert(0,body_copy[0]+self.direction)
#更新身体位置
self.body=body_copy[:]
换个角度我们可以优化一下这两块代码,在属性中设置一个标志isEat当其为0时,说明没有吃到食物,不改变身长执行移动步骤,当吃到食物后,将标志改为1,执行增加蛇长的步骤即可,优化后的代码如下:
def move_snake(self):
if self.isEat == 0:
# 取整条蛇的第一个到倒数第二个部分
body_copy = self.body[:-1]
body_copy.insert(0, body_copy[0] + self.direction)
# 更新身体位置
self.body = body_copy[:]
else:
# 取整条蛇部分
body_copy = self.body[:]
body_copy.insert(0, body_copy[0] + self.direction)
# 更新身体位置
self.body = body_copy[:]
self.isEat=0
食物
创建Food类表示和绘制出食物,用random.randint()随机绘制食物初始位置
属性:
- 食物初始位置
- 食物大小为一格
- 食物标记:0吃掉,1未吃掉
class Food:
# 初始化食物的位置,随机设置
def __init__(self):
self.x = random.randint(0, cell_num - 1)
self.y = random.randint(0, cell_num - 1)
self.pos = Vector2(self.x, self.y)
self.flag = 1
def draw_food(self):
# 创建食物实体,让食物占据一个格大小
food_rect = pygame.Rect(self.pos.x * cell_size, self.pos.y * cell_size, cell_size, cell_size)
# 画出食物实体
pygame.draw.rect(screen, orange, food_rect)
# 创建产生随机位置的方法
def randomize(self):
self.x = random.randint(0, cell_num - 1)
self.y = random.randint(0, cell_num - 1)
self.pos = Vector2(self.x, self.y)
主函数
在主函数部分,我们创建了新的蛇及食物对象,并在后面的游戏运行时使用全局调用,并且在创建update方法,在后续的游戏运行主体中,直接调用update并通过计时器触发update()中的所有后续定义的事件
class Main:
def __init__(self):
# 创建蛇和食物对象
self.snake = Snake()
self.food = Food()
# 使用全局生成新的调用和绘制
def update(self):
self.snake.move_snake()
self.eat_food()
def draw_elements(self):
self.snake.draw_snake()
self.food.draw_food()
吃食物
本块代码应在主函数部分在游戏中,我们需要判断蛇是否吃到食物,蛇头和食物是否重叠,依靠if判断语句,如果重叠(吃到食物),则会继续产生三件事,(1)生成新的食物(2)蛇身增长一格(3)分数加10
def eat_food(self):
# 判断蛇是否吃到食物的依据是,蛇头和食物是否重叠
if self.food.pos == self.snake.body[0]:
# 生成新的食物
self.food.randomize()
#蛇长加一
self.snake.isEat=1
死亡
在游戏规则中,游戏结束的条件有两个,撞到自己或撞墙(超出界面),判断是否撞墙的方法很简单,即蛇头的横纵坐标是否在0到cell_num之间,如果不在,则执行game_over方法。检测是否撞自己则是看蛇头是否撞击到自身的任何其他部位,这里用到了for循环。代码主要展示的是玩一遍死亡就退出的效果,如果想一直玩下去可以在Snake类增加复活机制,后续会作补充:
def fail_game(self):
# 是否撞墙
if not 0 <= self.snake.body[0].x < cell_num or not 0 <= self.snake.body[0].y <cell_num:
self.game_over()
# 撞自己
for block in self.snake.body[1:]:
if block == self.snake.body[0]:
self.game_over()
def game_over(self):
# 设置GameOver的显示颜色
gameover_Surf = BASICFONT.render('Game Over! Press space to restart!', True, orange)
# 设置GameOver的位置
gameover_Rect = gameover_Surf.get_rect()
gameover_Rect.midtop = (15 * cell_size, 15 * cell_size)
screen.blit(gameover_Surf, gameover_Rect)
pygame.display.flip()
time.sleep(3)
# 退出游戏
pygame.quit()
# 退出程序
sys.exit()
# self.snake.restart()
计分板与速度界面
def draw_score(self):
# (len(snake_Body) - 3) * 10
score = str((len(self.snake.body) - 3) * 10)
score_Surf = BASICFONT.render('score:%s' % (score), True, black)
score_Rect = score_Surf.get_rect()
score_Rect.midtop = (2 * cell_size, cell_size)
screen.blit(score_Surf, score_Rect)
def draw_speed(self):
speed = str(1 + len(self.snake.body) // 3)
speed_Surf = BASICFONT.render(f'speed:{speed}', True, black)
# 设置游戏速度的位置
speed_Rect = speed_Surf.get_rect()
speed_Rect.midtop = (28 * cell_size, cell_size)
screen.blit(speed_Surf, speed_Rect)
重复游戏版本:
前面说到,如果想让游戏在贪吃蛇死亡后自动重新开始,我们可以进行以下一些改动:
- 在Snake类我们为了让蛇头在每次重新开始游戏时都保持向右移动,会将移动方向的
self.direction = Vector2(1, 0)改为self.direction = Vector2(0, 0) - 定义restart()方法,也就是将初始设定重写一遍:
def restart(self):
self.body = [Vector2(5, 10), Vector2(4, 10), Vector2(3, 10)]
self.direction = Vector2(0, 0)
def game_over(self):
self.snake.restart()
- Main()类的game_over()方法只调用Snake中的restart()方法即可
运行主体
在游戏运行主体中,我们会先设置SCREEN_UPDATE事件并通过游戏运行主体中的event.type == USEREVENT触发贪吃蛇移动,通过event.type ==pygame.KEYDOWN来设置和键盘对应的移动方法,并且我们还要创建一个特殊条件,不允许蛇立即向相反方向移动,最后调用Main()中的各项方法:
SCREEN_UPDATE=pygame.USEREVENT
pygame.time.set_timer(SCREEN_UPDATE,150)
# 当时间达到150ms时会触发
if event.type == SCREEN_UPDATE:
main_game.update()
# 当键盘被按下按钮时会触发,且创建一个特殊条件,即不允许蛇立即向相反方向移动
if event.type == pygame.KEYDOWN: #触发按键事件
if event.key == pygame.K_UP:
if main_game.snake.direction.y != 1: #不允许立刻反向
main_game.snake.direction = Vector2(0, -1) # 向上移动
if event.key == pygame.K_DOWN:
if main_game.snake.direction.y != -1:
main_game.snake.direction = Vector2(0, 1) # 向下移动
if event.key == pygame.K_LEFT:
if main_game.snake.direction.x != 1:
main_game.snake.direction = Vector2(-1, 0) # 向左移动
if event.key == pygame.K_RIGHT:
if main_game.snake.direction.x != -1:
main_game.snake.direction = Vector2(1, 0) # 向右移动
screen.fill(purple) # 界面背景颜色
main_game.draw_elements() # 绘制图形
main_game.draw_score()
main_game.draw_speed()
pygame.display.update()
clock.tick(60)
如此一来,初步的贪吃蛇游戏便大功告成了,当然你也可以根据自己的想法和设计给贪吃蛇和食物增加图片设计,并且增添一些菜单模块,以便让游戏更加完善。我们将在后续探索如何加入算法,让电脑自己玩贪吃蛇。 #### ####
完整代码
import pygame
import random, sys, time
from pygame.locals import *
from pygame.math import Vector2
# 定义颜色块 RGB
green = pygame.Color(110, 150, 61)
orange = pygame.Color(255, 127, 80)
black = pygame.Color(0, 0, 0)
purple = pygame.Color(230, 230, 250)
grey = pygame.Color(105, 105, 105)
brown = pygame.Color(139, 129, 76)
blue = pygame.Color(70, 130, 180)
# pygame初始化
pygame.init()
cell_size = 20
cell_num = 30
score = 0
'''全局参数定义'''
# 定义窗口大小,标题,游戏界面内字体及大小
Screen_x = cell_num * cell_size
Screen_y = cell_num * cell_size
screen = pygame.display.set_mode((Screen_x, Screen_y))
pygame.display.set_caption("Snake Game")
BASICFONT = pygame.font.SysFont("times new roman", 20)
# 定义变量来控制游戏速度
clock = pygame.time.Clock()
# 定义食物图片
heart = pygame.image.load('aixin.png').convert_alpha()
heart = pygame.transform.scale(heart, (cell_size, cell_size))
'''创建并绘制蛇,包括蛇的属性,移动,吃食物,死亡等方法'''
class Snake:
def __init__(self):
# 定义蛇的长度,占3个方格,每个方格长度为10
self.body = [Vector2(5, 10), Vector2(4, 10), Vector2(3, 10)] # 头在身子的右边
# 定义蛇移动方向
self.direction = Vector2(1, 0)
self.isEat = 0 # 设置标志,0表示没吃到食物继续移动,1表示吃到食物增加蛇长
self.isDead = 0 # 设置标志,0表示没死,1表示死亡
self.head_up = pygame.transform.scale(pygame.image.load('head_up.png').convert_alpha(), (cell_size, cell_size))
self.head_down = pygame.transform.scale(pygame.image.load('head_down.png').convert_alpha(),
(cell_size, cell_size))
self.head_right = pygame.transform.scale(pygame.image.load('head_right.png').convert_alpha(),
(cell_size, cell_size))
self.head_left = pygame.transform.scale(pygame.image.load('head_left.png').convert_alpha(),
(cell_size, cell_size))
def draw_snake(self):
self.update_head_grah()
for index, block in enumerate(self.body):
x_pos = int(block.x * cell_size)
y_pos = int(block.y * cell_size)
block_rect = pygame.Rect(x_pos, y_pos, cell_size, cell_size)
if index == 0:
screen.blit(self.head, block_rect)
else:
pygame.draw.rect(screen, black, block_rect)
def update_head_grah(self):
head_relation = self.body[1] - self.body[0]
if head_relation == Vector2(1, 0):
self.head = self.head_left
elif head_relation == Vector2(-1, 0):
self.head = self.head_right
elif head_relation == Vector2(0, 1):
self.head = self.head_up
elif head_relation == Vector2(0, -1):
self.head = self.head_down
# 移动
def move_snake(self):
if self.isEat == 0:
# 取整条蛇的第一个到倒数第二个部分
body_copy = self.body[:-1]
body_copy.insert(0, body_copy[0] + self.direction)
# 更新身体位置
self.body = body_copy[:]
else:
# 取整条蛇部分
body_copy = self.body[:]
body_copy.insert(0, body_copy[0] + self.direction)
# 更新身体位置
self.body = body_copy[:]
self.isEat = 0
def restart(self):
self.body = [Vector2(5, 10), Vector2(4, 10), Vector2(3, 10)]
self.direction = Vector2(0, 0)
'''创建并绘制食物'''
class Food:
# 初始化食物的位置,随机设置
def __init__(self):
self.randomize()
self.flag = 1
# 创建产生随机位置的方法
def randomize(self):
self.x = random.randint(0, cell_num - 1)
self.y = random.randint(0, cell_num - 1)
self.pos = Vector2(self.x, self.y)
def draw_food(self):
# 创建食物实体,让食物占据一个格大小
food_rect = pygame.Rect(self.pos.x * cell_size, self.pos.y * cell_size, cell_size, cell_size)
# 画出食物实体
screen.blit(heart, food_rect)
# pygame.draw.rect(screen, orange, food_rect)
'''创建主函数'''
class Main:
def __init__(self):
# 创建蛇和食物对象
self.snake = Snake()
self.food = Food()
# 使用全局生成新的调用和绘制
def update(self):
self.snake.move_snake()
self.eat_food()
self.fail_game()
def draw_elements(self):
self.snake.draw_snake()
self.food.draw_food()
def eat_food(self):
# 判断蛇是否吃到食物的依据是,蛇头和食物是否重叠
if self.food.pos == self.snake.body[0]:
# 生成新的食物
self.food.randomize()
# 蛇长加一
self.snake.isEat = 1
# 不允许食物与蛇身重叠,若重叠则重新生成
for block in self.snake.body[1:]:
if block == self.food.pos:
self.food.randomize()
def fail_game(self):
# 是否撞墙
if not 0 <= self.snake.body[0].x < cell_num or not 0 <= self.snake.body[0].y < cell_num:
self.game_over()
# 撞自己
for block in self.snake.body[1:]:
if block == self.snake.body[0]:
self.game_over()
def game_over(self):
# 设置GameOver的显示颜色
gameover_Surf = BASICFONT.render('Game Over!', 40, True, orange)
# 设置GameOver的位置
gameover_Rect = gameover_Surf.get_rect()
gameover_Rect.midtop = (15 * cell_size, 15 * cell_size)
screen.blit(gameover_Surf, gameover_Rect)
pygame.display.flip()
while True: # 键盘监听事件
for event in pygame.event.get(): # event handling loop
if event.type == QUIT:
pygame.quit()
sys.exit()
elif event.type == KEYDOWN:
if event.key == K_ESCAPE or event.key == K_q: # 终止程序
pygame.quit()
sys.exit()
def game_reset(self):
self.snake.restart() # 重新开始游戏
def draw_score(self):
# (len(snake_Body) - 3) * 10
score = str((len(self.snake.body) - 3) * 10)
score_Surf = BASICFONT.render('score:%s' % (score), True, black)
score_Rect = score_Surf.get_rect()
score_Rect.midtop = (2 * cell_size, cell_size)
screen.blit(score_Surf, score_Rect)
def draw_speed(self):
speed = str(1 + len(self.snake.body) // 3)
speed_Surf = BASICFONT.render(f'speed:{speed}', True, black)
# 设置游戏速度的位置
speed_Rect = speed_Surf.get_rect()
speed_Rect.midtop = (28 * cell_size, cell_size)
screen.blit(speed_Surf, speed_Rect)
# 创建新的贪吃蛇和食物对象
main_game = Main()
# 通过计时器触发移动
SCREEN_UPDATE = pygame.USEREVENT
pygame.time.set_timer(SCREEN_UPDATE, 150)
# 游戏运行主体
game_flag = True
while game_flag:
# 在游戏开始前,尽可能检查所有pygame.event.get():中的事件
for event in pygame.event.get():
if event.type == QUIT:
# 接收到退出事件后,退出程序
pygame.quit()
sys.exit()
# 当时间达到150ms时会触发
if event.type == SCREEN_UPDATE:
main_game.update()
# 当键盘被按下按钮时会触发,且创建一个特殊条件,即不允许蛇立即向相反方向移动
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP:
if main_game.snake.direction.y != 1:
main_game.snake.direction = Vector2(0, -1) # 向上移动
if event.key == pygame.K_DOWN:
if main_game.snake.direction.y != -1:
main_game.snake.direction = Vector2(0, 1) # 向下移动
if event.key == pygame.K_LEFT:
if main_game.snake.direction.x != 1:
main_game.snake.direction = Vector2(-1, 0) # 向左移动
if event.key == pygame.K_RIGHT:
if main_game.snake.direction.x != -1:
main_game.snake.direction = Vector2(1, 0) # 向右移动
screen.fill(purple) # 界面背景颜色
main_game.draw_elements() # 绘制图形
main_game.draw_score()
main_game.draw_speed()
pygame.display.update()
clock.tick(100)