大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。
Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺
跳转目录
前言
该程序未使用pygame库, 而是采用keyboard库的
on_press()
函数捕获键盘操作, 使用windows控制台缓冲区作为图形界面。
第一次写文章,如有错误请谅解
运行示例
程序分析
捕获键盘操作
Python从控制台读取可以使用
input()
函数, 但很明显的是, 贪吃蛇游戏需要在游戏运行的同时不断读取键盘操作, 使用input()输入需要每次都使用回车键, 可行性较差
本程序中使用keyboard库中的on_press()
函数捕获键盘操作, 并将捕获的键盘操作传递给一个key_envent()
函数进一步处理, 进而控制游戏
输出游戏画面
贪吃蛇游戏需要不断的更新和显示游戏画面, 游戏地图可以使用二维矩阵的形式储存, 显示画面时, 将矩阵中的信息转换成字符串并输出即可
代码分析一
安装运行环境
# 测试python版本为3.9.8
pip install keyboard
pip install win32
游戏地图的实现
创建地图需要的基本信息包括宽度width, 以及高度height
地图的每个格子代表一种元素,0
代表空格,1
代表食物,2
代表炸弹,3
代表蛇的头部,4
代表蛇的身体
对地图的操作包括读取(x, y)
位置的元素, 改变(x, y)
位置的元素, 以及将列表输出为可以显示的形式
- 地图的初始化
class Map():
def __init__(self, width=10, height=10):
if (width < 10):
width = 10
if (height < 10):
height = 10
self.size = (width, height)
self.__map = [[0 for i in range(width)] for i in range(height)]
# 0为空白块,1为食物,2为炸弹,3为蛇头,4为蛇尾
- 读取某位置的元素
def read(self, x, y):
if (x >= 0 and x < self.size[0] and y >= 0 and y < self.size[1]):
return self.__map[y][x]
return -1 # 如果该位置超过地图边界, 返回-1
- 改变某位置的元素
def write(self, x, y, val=0):
self.__map[y][x] = val
- 将地图以可显示形式输出
输出部分为按行输出(便于后续图形界面的排版), 将每行结果储存在列表中并返回
def list(self):
ls = []
ls.append('# ' * (self.size[0] + 2)) # 地图上边界
for line in self.__map:
li = '# '
for k in line:
if (k == 0): # 0表示空白块
li += ' '
elif (k == 1): # 1表示食物
li += "3[0;32m$3[0m "
elif (k == 2): # 2表示炸弹
li += "3[0;31m@3[0m "
elif (k == 3): # 3表示蛇头
li += "3[0;33m■3[0m "
elif (k == 4):# 4代表蛇的身体
li += "3[0;36m■3[0m "
li += '#'
ls.append(li)
ls.append('# ' * (self.size[0] + 2)) # 地图下边界
return ls
简单测试打印一下地图
ma = Map(20, 20)
ls = ma.list()
for line in ls:
print(line)
炸弹的实现
对于单个炸弹, 其包含的信息有在地图中的位置
x
y
剩余存在时间life
炸弹生成时位置应该保持随机, 并且只能在空白块处生成
class Bomb():
def __init__(self, map : Map):
self.x = randint(0, map.size[0] - 1)
self.y = randint(0, map.size[1] - 1)
while (map.read(self.x, self.y) != 0): # 读取地图中该位置是否为空格
self.x = randint(0, map.size[0] - 1)
self.y = randint(0, map.size[1] - 1)
self.life = randint(3, 6) # 随机的存活时间
游戏地图中显然炸弹有多个, 因此创建一个
Bombs
类用于处理地图中的全部炸弹
每一帧游戏需要对全部炸弹进行更新, 更新操作包含生成新的炸弹, 重新计算炸弹存在时间, 将炸弹显示在地图上
class Bombs():
def __init__(self):
self.list = [] # 储存每一个炸弹的信息
def update(self, map : Map):
tmp = self.list.copy() # 将炸弹的信息拷贝到一个临时列表中
self.list.clear()
if (randint(0, 49) == 0): # 按概率每秒生成一个新的炸弹
tmp.append(Bomb(map))
for bomb in tmp: # 遍历每一个炸弹
map.write(bomb.x, bomb.y, 0) # 先将炸弹位置的地图重置
bomb.life -= 1 / 50 # 计算存在时间
if (bomb.life > 0): # 如果存在时间大于零将其加入到炸弹列表中
self.list.append(bomb)
del tmp
for bomb in self.list: # 将炸弹显示在地图上
map.write(bomb.x, bomb.y, 2)
食物的实现
食物的实现思路与炸弹的实现基本相同, 但是食物可以被蛇吃掉, 所以食物需要增添一个
eat()
方法
- 单个食物的实现
不能说和炸弹很相似, 只能说是一模一样
class Food():
def __init__(self, map : Map):
self.x = randint(0, map.size[0] - 1)
self.y = randint(0, map.size[1] - 1)
while (map.read(self.x, self.y) != 0):
self.x = randint(0, map.size[0] - 1)
self.y = randint(0, map.size[1] - 1)
self.life = randint(3, 6)
- 全部食物信息的实现, 相较于炸弹类, 仅多一个
eat()
方法
class Foods():
def __init__(self):
self.list = []
def update(self, map : Map):
tmp = self.list.copy()
self.list.clear()
if (randint(0, 49) == 0):
tmp.append(Food(map))
for food in tmp:
map.write(food.x, food.y, 0)
food.life -= 1 / 50
if (food.life > 0):
self.list.append(food)
del tmp
for food in self.list:
map.write(food.x, food.y, 1)
def eat(self, x, y): # 将坐标处被吃掉的食物的存在时间变为0, 下一次更新时食物会被删除
for index, food in enumerate(self.list):
if (food.x == x and food.y == y):
self.list[index].life = 0
蛇的实现
既然是贪吃蛇, 最重要的自然是蛇
蛇有两个部分组成, 分别是蛇头head
和蛇的身体body
, 蛇头需要储存的信息为位置[x, y,]
和方向
, 蛇的身体由多节组成, 每一节身体都需要储存其位置[x, y]
- 蛇的初始化
class Snake():
def __init__(self, map : Map):
# [x, y], 创建蛇时需要随机蛇头的位置和方向
self.__head = [randint(3, map.size[0] - 5), randint(3, map.size[1] - 5)] # 随机时需要防止太靠近边界导致开局碰墙
self.__direction = randint(1, 4)
# [[x, y], [x, y], ....]
self.__body = [] # 开始游戏时蛇的身体长度为0
蛇的主要操作为移动
move()
, 在移动时会触发各种场景
- 蛇头移向空白处, 即移动后蛇头位置处的地图为空白块, 蛇整体移动一格
- 蛇头移向食物处, 即移动后蛇头位置处的地图为食物, 蛇长度增长一格并整体前进一格, 同时触发食物的
eat()
操作, 吃掉该位置处的食物- 蛇头移向炸弹处, 即移动后蛇头位置处的地图为炸弹, 游戏结束
- 蛇头移向墙, 即移动后蛇头位置的位置超过地图边界, 游戏结束
- 蛇头移向蛇身体, 即移动后蛇头位置处的地图为蛇身体, 游戏结束
蛇向前移动时, 并不需要改变每一部分身体的位置, 只需在身体的最前方添加一节身体, 位置与原蛇头位置相同,
如果蛇没有变长, 删除最后一节蛇尾即可
,如果蛇变长, 不用删除最后一节蛇尾
- 蛇身体的移动
def move(self, map : Map, direction=0):
self.__body.insert(0, [self.__head[0], self.__head[1]])
map.write(self.__body[0][0], self.__body[0][1], 4) # 第一节身体位置移动到原蛇头位置
map.write(self.__body[-1][0], self.__body[-1][1], 0) # 删除最后一节蛇尾位置
- 蛇头根据给定方向移动
if (direction != 0): # 为0时表示无方向输入, 按照原来的轨迹移动
self.__direction = direction
if (self.__direction == 1): # 向上
self.__head[1] -= 1
elif (self.__direction == 2): # 向下
self.__head[1] += 1
elif (self.__direction == 3): # 向左
self.__head[0] -= 1
elif (self.__direction == 4): # 向右
self.__head[0] += 1
- 读取蛇头移动后位置处地图的情况
result = map.read(self.__head[0], self.__head[1]) # 移动结果
- 根据移动情况判断游戏下一步操作
longer = False # 是否变长
move = True # 是否能够移动
tip = "just move" # 提示信息
if (result == -1): # 碰墙
move = False
tip = "hit the wall"
elif (result == 1): # 碰到食物
longer = True
tip = "eat food"
elif (result == 2): # 碰到炸弹
move = False
tip = "hit the bomb"
elif (result == 4): # 碰到蛇尾
move = False
tip = "eat your body"
else:
pass
- 根据移动情况判断蛇尾是否变化, 以及返回移动信息
(提示词, (移动后蛇头的坐标x, y))
if (move): # 是否能够移动
if (not longer): # 是否变长
self.__body.pop()
else:
map.write(self.__body[-1][0], self.__body[-1][1], 4)
map.write(self.__head[0], self.__head[1], 3)
return (tip, (self.__head[0], self.__head[1]))
初步测试
此时游戏所需的地图, 食物, 炸弹等已经全部实现, 可通过简单代码进行初步测试
game_map = Map(20, 20) # 初始化地图
foods = Foods() # 初始化食物
bombs = Bombs() # 初始化炸弹
snake = Snake(game_map) # 初始化蛇
tick = 0 # 游戏刻, 用于控制蛇的移动速度
while True:
move = ("just move", (0, 0)) # 用来记录蛇move之后的信息
if (tick == 0): # 0刻时蛇移动一次
move = snake.move(game_map, randint(1, 4))
if (move[0] == "eat food"): # 吃到食物执行eat()操作
foods.eat(move[1][0], move[1][1])
elif (move[0] != "just move"): # 触发游戏结束条件
break
foods.update(game_map) # 更新食物
bombs.update(game_map) # 更新炸弹
ls = game_map.list() # 地图可视化
for line in ls:
print(line)
tick = (tick + 1) % 5 # 游戏刻加一
time.sleep(0.02) # 控制游戏帧率
os.system("cls") # 清屏
- 运行效果
基本上已经正常了, 再加上键盘操作即可控制蛇的移动
但有一个明显的问题,print()
+clear
操作闪瞎玩家的眼睛会导致屏幕严重闪烁, 产生该问题的原因是清除控制台再重新输出不是瞬间完成, 为解决该问题需要使用双缓冲DoubleBuffer
, 当前缓冲区显示, 下一个缓冲区更新完成后直接替换该缓冲区的内容, 即可解决屏幕更新不及时造成的闪烁问题
双缓冲的实现参考Python控制台双缓冲Double Buffer
本文章直接调用Buffers()
类, 不再进行额外介绍
键盘控制的实现
到现在为止, 虽然蛇已经可以移动, 吃食物, 游戏判断等等, 但是蛇的移动是不受玩家控制的
控制蛇的移动需要不断读取键盘操作, 并将键盘操作处理后传递给Snake.move()
keyboard中的keyboard.on_press(call)
可以绑定一个函数call(x)
, 每次有按键按下时将会执行call(x)
, 参数x
为键盘事件, 读取x.name
即可获得按下按键的名称
key_event()
函数
def key_envent(key):
global direction # 全局变量direction, Snake.move()的方向参数
global gaming # 全局变量gaming, 记录游戏是否正在运行, 以及结束游戏
global pause # 全局变量pause, 用于游戏的暂停操作
if (key.name == "up"): # 按上方向键
direction = 1
elif (key.name == "down"): # 按下方向键
direction = 2
elif (key.name == "left"): # 按左方向键
direction = 3
elif (key.name == "right"): # 按右方向键
direction = 4
elif (key.name == "space"): # 按空格键, 暂停/继续
pause = not pause
elif (key.name == "esc" and gaming): # 按ESC键退出游戏
gaming = False
keyboard.on_press()
绑定
keyboard.on_press()
绑定key_event()
函数后, 每一次按下键盘按键都会执行key_event()
函数, 直到程序的主进程退出
keyboard.on_press(key_envent)
主程序
上文中已经实现了游戏的基本流程和键盘操作, 实现游戏的主程序之后即可正常游玩
部分内容本文未作详细解释, 请参考源码使用
game函数
将所有的游戏内流程, 如创建各种对象, 各种对象的更新封装在
game()
函数中, 方便多次重复游戏
该部分代码为测试代码的扩充
加入了多缓冲区, 游戏暂停, 固定时间刷新画面内容等
def game():
global direction # 方向
global gaming # 游戏是否在进行
global pause # 是否暂停
buffers = Buffers() # 创建一个双缓冲区用于显示游戏画面
game_map = Map(20, 20) # 指定大小创建游戏地图
bombs = Bombs()
foods = Foods()
snake = Snake(game_map)
tick = 0
direction = 0
score = 0 # 记录游戏得分
tip = "" # 记录游戏退出时的提示次
gaming = True
pause = False
start_time = time.time()
while gaming: # 如果游戏结束退出循环
if (pause): # 游戏暂停, 休眠一秒后再判断pause的状态, 降低计算消耗
start_time += 1 # 休眠时时间不流动
time.sleep(1)
continue
loop_time = time.perf_counter() # 记录循环开始时间
move = ("just move", (0, 0))
if (tick == 0):
move = snake.move(game_map, direction)
if (move[0] == "eat food"):
foods.eat(move[1][0], move[1][1])
score += 1
elif (move[0] != "just move"):
tip = move[0]
gaming = False
break
foods.update(game_map)
bombs.update(game_map)
buffers.switch() # 切换画面缓冲区
map_ls = show_info(game_map.list(), score, int(time.time() - start_time)) # 在游戏地图后添加游戏时间, 游戏得分, 排版游戏画面
for line in map_ls: # 将游戏画面输出到下一个缓冲区
buffers.print(line+'\n')
buffers.print("ESC键退出游戏 空格键暂停\\继续")
buffers.flash() # 刷新游戏画面
tick = (tick + 1) % 5
time.sleep(0.02 - (loop_time - time.perf_counter())) # 按照固定时间(0.02s)运行游戏程序, 即指定游戏帧数
end(tip, score, map_ls) # 执行结束函数显示提示信息
图形界面显示分数, 得分
- 向该函数输入转换后的地图列表, 游戏时间, 分数信息, 返回一个新的地图列表, 列表中包含游戏的时间 T 和游戏分数 S
def show_info(map_ls, score, game_time):
pass
return map_ls
结束函数
游戏结束后打印地图并显示提示语
def end(tip, score, map_ls):
os.system("cls")
for line in map_ls:
print(line)
if (tip == "hit the wall"):
print("\033[0;31m您撞墙后不治身亡!\033[0m")
elif (tip == "hit the bomb"):
print("\033[0;31m炸弹真美味, 可惜会爆炸\033[0m")
elif (tip == "eat your body"):
print("\033[0;31m您真狠, 饿了连自己都不放过\033[0m")
elif (tip == ""):
print("\033[0;31m请问你为什么要退出游戏呢?\033[0m")
print("\033[0;33m游戏结束\033[0m")
print("\033[0;34m您的得分为: \033[0;32m{}\033[0m".format(score))
print("\033[0;33m输入任意内容退出游戏 \033[0;32m输入\033[0;34m空格\033[0;32m重新开始游戏\033[0m")
游戏主函数
在主函数中绑定键盘操作, 判断是否继续下一次游戏等
def main():
keyboard.on_press(key_envent) # 绑定键盘操作
while True: # 实现游戏的多次
game() # 执行游戏函数
if (input("\n") != " "): # 根据输入内容判断是否进行下一次游戏
break
main() # 运行主函数
源码下载
希望本文对您有所帮助, 感谢您花时间浏览本文
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/169552.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...