一看就懂的,Python迷宫小游戏教程
前言
代码量1K行,主要使用pygame模块库(python第三方模块库,自行安装)、pycharm工具(自己喜欢用~),进行2D迷宫小游戏的编写。代码有很多备注,我做的时候由于技术不过关,没有加额外道具,只有一把像素道具=。=,学会了后大家可以自行添加你喜欢的样式与众不同。本文主要讲程序设计流程和模块具体作用,这样你写的时候就很快融合,模块怎么起的作用,开始这篇博客吧。
开发工具
参考资料(真的看完这些资料,结合你的语言基础,写出来超级简单)
Python迷宫游戏(基础版)
Python 函数查询网站
pygame之time模块
用PyInstaller将.py文件打包成.exe文件
python os.path模块常用方法详解
python assert的作用
Pygame详解(五):font 模块
pygame学习笔记(1)——安装及矩形、圆型画图
三大迷宫生成算法 (Maze generation algorithm) – 深度优先,随机Prim,递归分割
只记得这么多了,见谅,收藏夹都被我翻遍了。。。。
Python第三方模块库安装
游戏开发总共使用到了random, sys, copy, pygame等模块。
安装方法有pip install xxx和pip install xxxx.whl 两种方法,当第一种失败后那么你就需要采取第二种方法了。第一种模块安装方法详见 安装方法
第二种 从网站中去找对应的whl文件安装,pip install XXX
冲!!!
建议没有学过python的py去看廖雪峰老师的python教程,我就是从这里起步的,然后可以适当的了解一下所用到的第三方模块库,我的建议是,多读案例多跟着写,有一些算法问题想想,想不通的建议放一会儿,酝酿一下自通。
主要学习一下pygame模块的相干使用,封装的很好,非常的容易上手,不得不吹一波,如果再结合pyode进行3D游戏开发,大家可以自行搜索参考。
首先有个项目大家先读懂,python推箱子,这个项目你看懂了,那你离迷宫游戏完成就差百分之50了,主要借鉴这个项目的图层的绘制,python的event事件。会有很大启发,列如:图层的绘制,方法是将数张小图拼接在一起变成一张大图—地图,那小图也是更小的图组合的咯,那就是像素,你琢磨一下是不是这个道理,点及线,线及面,面及体。这里顺嘴一提人物和墙壁的碰撞识别也是和图片位置(x,y)相关的,比如(2,3)的图是石头,那(2,3)这个点就不能走。
程序中的问题
如截图所示,这个程序有一个功能是人物自动寻找出口并行走,直白就是在数组中寻找正确的路径。由于我在算法上遇到了一些问题所以采取了另外一种办法:在迷宫生成的同时记录下迷宫出口的正确路径,通过这个记录实现功能,其实就是读数组的一个过程(这就不是寻找迷宫出路了,说是随机迷宫,但其实都是自己生成的,就不是很满意)。但是以上被我注释的代码是对于任何迷宫地图都可以找到正确的路径,使用的是遍历的思想,但是也出现了问题,解决方法也留给各位小伙伴了,哈哈哈,偷个懒。
一、
迷宫迷宫,首先生成迷宫对吧,随机迷宫的思想我上文已经说了,下面直接上代码。
def map(x,y):
sx = x
sy = y
global end,maxpath,copymaxpath,endpath
end = [[2,2]]
endpath = []
maxpath = 1
copymaxpath = 0
historyhouse = [[0 for col in range(sx)] for row in range(sy)] # 创建二维数组10*10
maze = [[' ' for col in range(2 * sx + 3)] for row in range(2 * sy + 3)] # 地图21*21
operation = {1: (0, -1), 2: (0, 1), 3: (-1, 0), 4: (1, 0)} # 移动,使用坐标方式左右上下
direction = [1, 2, 3, 4]
for i in range(2 * sx + 2):
if i % 2 == 1:
for j in range(2 * sx + 3):
maze[i][j] = '#'
for i in range(2 * sy + 2):
if i % 2 == 1:
for j in range(2 * sy + 3):
maze[j][i] = '#'
# 创建墙壁,应用算法
def generateMaze(start):#递归点,生成地图
global end,maxpath,copymaxpath,endpath
x, y = start
traversenum = 0
historyhouse[x][y] = 1
random.shuffle(direction) # shuffle() 方法将序列的所有元素随机排序
for d in direction:
px, py = (x + y for x, y in zip(start, operation[d])) # (11,11)和(0,-1 )或者别的
if px < 0 or px >= sx or py < 0 or py >= sy: # 选取24值是执行
if copymaxpath >= maxpath:
maxpath = copymaxpath
endpath = end
else:
if historyhouse[px][py] is not 1:
mx = 2 * x + 2
my = 2 * y + 2
if d == 1:
mx, my = mx, my - 1
elif d == 2:
mx, my = mx, my + 1
elif d == 3:
mx, my = mx - 1, my
elif d == 4:
mx, my = mx + 1, my
maze[mx ][my ] = ' '
end.append([mx,my])
copymaxpath += 1
traversenum += 1
generateMaze((px, py))
if traversenum == 0:
copymaxpath = 0
end = [[2,2]]
generateMaze((0, 0))
pathx, pathy = endpath[-1]
if endpath[-1] == [2,2]:
end = [[2, 2]]
endpath = []
maxpath = 1
copymaxpath = 0
generateMaze((0, 0))
maze[pathx][pathy] = '.'
maze[2][2] = '@'
'''readgoldsnum=1
for i in range((2*x)):
readgoldsnum+=1
if maze[(2*x )][(2*x - i)] == '#':
if maze[(2*x )][((2*x - 1) - i)] != '#':
maze[(2*x )][((2*x ) - i)] = '.'
break
if readgoldsnum==2*x+1:
for i in range((2*x )):
if maze[(2*x )][((2*x ) - i)] == '#':
maze[(2*x )][((2*x ) - i)] = '.'
break
if '.' not in maze:
map(x,y)'''
return maze
def isWall(mapObj, x, y):
if x < 0 or x >= len(mapObj) or y < 0 or y >= len(mapObj[x]):
return False #
elif mapObj[x][y] in ('#','x'):
return True #
return False
map函数用于生成指定大小的迷宫。iswall函数用于判断为墙的字符是否出现,上文说过,所有可视物体都是图像,而图像是点的集合,定位图像就是定位点,代码中代表点的数组,就是地图数组,数组中不同的字符就是特殊图像。
执行genemaze(2,2)便有地图输出。
修饰这个地图
先上代码
import random,copy
def floodFill(mapObj, x, y, oldchar, newchar):
if mapObj[x][y] == oldchar:
mapObj[x][y] = newchar
if x < len(mapObj) - 1 and mapObj[x+1][y] == oldchar:
floodFill(mapObj, x + 1, y, oldchar, newchar)
if x > 0 and mapObj[x-1][y] == oldchar:
floodFill(mapObj, x - 1, y, oldchar, newchar)
if y < len(mapObj[x]) - 1 and mapObj[x][y+1] == oldchar:
floodFill(mapObj, x, y + 1, oldchar, newchar)
if y > 0 and mapObj[x][y-1] == oldchar:
floodFill(mapObj, x, y - 1, oldchar, newchar)
def decorateMap(mapObj, startxy):
startx, starty = startxy
global biggoldnum,biggoldx,biggoldy
biggoldnum = 0
mapobjcopy = copy.deepcopy(mapObj)
for x in range(len(mapobjcopy)):
for y in range(len(mapobjcopy[0])):
if mapobjcopy[x][y] in ( '.', '@', '+'):
mapobjcopy[x][y] = ' '
floodFill(mapobjcopy, startx, starty, ' ', 'o')
# 将相邻的墙壁转换为角落瓷砖。
for x in range(len(mapobjcopy)):
for y in range(len(mapobjcopy[0])):
if mapobjcopy[x][y] == '#':
if (isWall(mapobjcopy, x, y-1) and isWall(mapobjcopy, x+1, y)) or \
(isWall(mapobjcopy, x+1, y) and isWall(mapobjcopy, x, y+1)) or \
(isWall(mapobjcopy, x, y+1) and isWall(mapobjcopy, x-1, y)) or \
(isWall(mapobjcopy, x-1, y) and isWall(mapobjcopy, x, y-1)):
mapobjcopy[x][y] = 'x'
#elif mapobjcopy[x][y] == ' ' and random.randint(0, 99) < OUTSIDE_DECORATION:
#mapobjcopy[x][y] = random.choice(list(OUTSIDETREESMAP.keys()))
elif mapobjcopy[x][y] == 'o' and random.randint(0, 99) < 10:
if random.randint(0,10) > 8 and random.randint(0,10) < 10:
if biggoldnum < 1:
mapobjcopy[x][y] = '*'
biggoldx = x
biggoldy = y
biggoldnum += 1
else:
mapobjcopy[x][y] = 'g'
return mapobjcopy
floodFill函数是用来判断地图内外的,举例(没有经过判断的地图数组)
[[’ ', ‘#’, ’ ', ‘#’, ’ ', ‘#’, ’ ', ‘#’, ’ ', ‘#’, ’ ‘, ‘#’, ’ ‘, ‘#’, ’ ‘],
[’#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’],
[’ ', ‘#’, ‘@’, ‘#’, ’ ', ’ ', ’ ', ’ ', ’ ', ‘#’, ’ ', ‘.’, ’ ‘, ‘#’, ’ ‘],
[’#’, ‘#’, ’ ', ‘#’, ’ ', ‘#’, ‘#’, ‘#’, ’ ', ‘#’, ’ ‘, ‘#’, ‘#’, ‘#’, ‘#’],
[’ ', ‘#’, ’ ', ‘#’, ’ ', ’ ', ’ ', ‘#’, ’ ', ‘#’, ’ ', ’ ', ’ ‘, ‘#’, ’ ‘],
[’#’, ‘#’, ’ ', ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ’ ', ‘#’, ‘#’, ‘#’, ’ ‘, ‘#’, ‘#’],
[’ ', ‘#’, ’ ', ’ ', ’ ', ’ ', ’ ', ’ ', ’ ', ’ ', ’ ', ‘#’, ’ ‘, ‘#’, ’ ‘],
[’#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ’ ', ‘#’, ’ ‘, ‘#’, ‘#’],
[’ ', ‘#’, ’ ', ’ ', ’ ', ‘#’, ’ ', ’ ', ’ ', ‘#’, ’ ', ‘#’, ’ ‘, ‘#’, ’ ‘],
[’#’, ‘#’, ’ ', ‘#’, ’ ', ‘#’, ’ ', ‘#’, ‘#’, ‘#’, ’ ', ‘#’, ’ ‘, ‘#’, ‘#’],
[’ ', ‘#’, ’ ', ‘#’, ’ ', ’ ', ’ ', ‘#’, ’ ', ’ ', ’ ', ‘#’, ’ ‘, ‘#’, ’ ‘],
[’#’, ‘#’, ’ ', ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ’ ', ‘#’, ’ ', ‘#’, ’ ‘, ‘#’, ‘#’],
[’ ', ‘#’, ’ ', ’ ', ’ ', ’ ', ’ ', ’ ', ’ ', ‘#’, ’ ', ’ ‘, ’ ‘, ‘#’, ’ ‘],
[’#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’, ‘#’],
[’ ', ‘#’, ’ ', ‘#’, ’ ', ‘#’, ’ ', ‘#’, ’ ', ‘#’, ’ ', ‘#’, ’ ‘, ‘#’, ’ ‘]]
显而易见,@是人所在位置,.是终点,想要修饰我们的地图是不是得知道哪里是地图外面哪里是地图里面,可是里外地板都是空格表示得,根本无法分辨。幸运的是我们有一个条件可以用于判断,那就是内外由墙壁也就是#字符分割开了,于是我们使用洪水灌溉算法来判断地图内外,并替换掉里面地板的字符。修饰后的地图数组(不是同一个数组了,随机生成的…):
[[’ ‘, ‘#’, ’ ‘, ‘#’, ’ ‘, ‘#’, ’ ‘, ‘#’, ’ ‘, ‘#’, ’ ‘, ‘#’, ’ ‘, ‘#’, ’ ‘],
[’#’, ‘x’, ‘#’, ‘x’, ‘#’, ‘x’, ‘#’, ‘x’, ‘#’, ‘x’, ‘#’, ‘x’, ‘#’, ‘x’, ‘#’],
[’ ‘, ‘#’, ‘o’, ‘#’, ‘o’, ‘o’, ‘o’, ‘o’, ‘o’, ‘o’, ‘g’, ‘g’, ‘o’, ‘#’, ’ ‘],
[’#’, ‘x’, ‘o’, ‘#’, ‘o’, ‘x’, ‘#’, ‘#’, ‘#’, ‘x’, ‘o’, ‘#’, ‘o’, ‘x’, ‘#’],
[’ ‘, ‘#’, ‘g’, ‘#’, ‘o’, ‘#’, ‘o’, ‘o’, ‘o’, ‘#’, ‘o’, ‘#’, ‘o’, ‘#’, ’ ‘],
[’#’, ‘x’, ‘o’, ‘x’, ‘#’, ‘x’, ‘o’, ‘#’, ‘o’, ‘x’, ‘#’, ‘x’, ‘o’, ‘x’, ‘#’],
[’ ‘, ‘#’, ‘o’, ‘#’, ‘o’, ‘o’, ‘o’, ‘#’, ‘o’, ‘g’, ‘o’, ‘o’, ‘o’, ‘#’, ’ ‘],
[’#’, ‘x’, ‘o’, ‘#’, ‘o’, ‘x’, ‘#’, ‘x’, ‘#’, ‘#’, ‘#’, ‘#’, ‘g’, ‘x’, ‘#’],
[’ ‘, ‘#’, ‘o’, ‘o’, ‘o’, ‘#’, ‘o’, ‘#’, ‘o’, ‘o’, ‘o’, ‘o’, ‘o’, ‘#’, ’ ‘],
[’#’, ‘x’, ‘#’, ‘x’, ‘#’, ‘x’, ‘o’, ‘#’, ‘o’, ‘x’, ‘#’, ‘#’, ‘#’, ‘x’, ‘#’],
[’ ‘, ‘#’, ‘o’, ‘#’, ‘o’, ‘o’, ‘o’, ‘o’, ‘o’, ‘#’, ‘o’, ‘o’, ‘o’, ‘#’, ’ ‘],
[’#’, ‘x’, ‘o’, ‘#’, ‘o’, ‘#’, ‘o’, ‘#’, ‘#’, ‘x’, ‘#’, ‘#’, ‘g’, ‘x’, ‘#’],
[’ ‘, ‘#’, ‘*’, ‘o’, ‘o’, ‘#’, ‘o’, ‘o’, ‘o’, ‘o’, ‘o’, ‘o’, ‘o’, ‘#’, ’ ‘],
[’#’, ‘x’, ‘#’, ‘x’, ‘#’, ‘x’, ‘#’, ‘x’, ‘#’, ‘x’, ‘#’, ‘x’, ‘#’, ‘x’, ‘#’],
[’ ', ‘#’, ’ ', ‘#’, ’ ', ‘#’, ’ ', ‘#’, ’ ', ‘#’, ’ ', ‘#’, ’ ', ‘#’, ’ ']]
这次是不是很显然就看出来迷宫出口路径了啊,嘿嘿嘿。没有@符号了,是和地图绘制有关的,大概原因为地图的窗户显示是先画地板在画人,但是字符只能存在一个,所以保留地板,使用变量记录人物位置,记录的过程与下章的readlevelsarray函数相关,再细说。X符号是表示拐角的墙壁,也是修饰用,代码思想是:这个点如果两个相邻的点(比如左和上、上和左、上和右)为墙壁#那就是拐角。
random是一个随机模块,网上有很多教程我不赘述。copy模块的作用建议自行创建一个二维数组同环境修改测试一下便知,不用的结果我先说,形参会影响到实参的值。
读地图数组中的重要数据,与声明关键变量
def readlevelsarray(filename):
startx = None # 定义玩家起始位置的x和y
starty = None
goal = (0,0)
for x in range((2*maphardx+1)):
for y in range((2*maphardx+1)):
if filename[x][y] == '@':#读取玩家的初始位置
startx = x
starty = y
if filename[x][y] == '.':#中点位置
goal = (x,y)
gamestateobj = { 'player': (startx, starty),#存储有关人物的量
'stepCounter': 0,
'biggold': 0,
'score': 0,
'axe': 0 }
mapdate = {#存贮所有和地图相关的量
'height': len(filename),
'mapobj': filename,
'goal': goal,
'startState': gamestateobj}
assert len(goal)>0, 'the map do not have goal'
return mapdate
为什么这里要去保存一些数据和声明一些变量呢。1、上面说了,人物字符在数组中被取消了,所以需要一个地方来保存这些信息,就好比这个班的张三,父亲母亲是谁,什么职业,自身成绩如何等等,每一个地图也有自己的信息,这些信息在许多地方都会被使用到,比如biggold,是游戏中的隐藏金币,判断隐藏金币死否还存在就看它的值,给玩家提示。2、可以快捷的判断出地图数组是否是完整的,随机就是随机的,如果生成过程中可能根本就没有生成金币,那就是bug了,assert是个好东西。
绘制地图在屏幕上
常规操作,创建创建屏幕,设置名称图标等等,不赘述。
def main():
global WINCLOCK, SCREEN, locationimage, tileimage, outsidedecorate, BASICFONT, whoplayer, currentplayer,GOLD,refreshmapnum,endpath,maphardx,maphardy
# 系统时钟,screen,图像位置,地图, ,文字图像,玩家,索引
# 初始化
pygame.init()
pygame.mixer.init()
pygame.time.delay(1000)
#设置一个时钟,用于跟踪运行速度,可起延时作用
WINCLOCK = pygame.time.Clock()
refreshmapnum=0
maplist = []
#
SCREEN = pygame.display.set_mode((SCWIDTH, SCHEIGHT))#设置一个屏幕
pygame.display.set_caption('Start maze')#游戏名称设置
iconimage = pygame.image.load(r'C:\maze\sea1.jpg')#设置图标
pygame.display.set_icon(iconimage)
BASICFONT = pygame.font.Font(r'C:\maze\Lib\site-packages\pygame\freesansbold.ttf', 18)#设置一个文本实例
这个不懂的老铁,先学一下pygame模块的基础,参考资料里面有推荐的。
正文
def drawmap(mapobj, gamestateobj, goal):
#计算地图的大小,生成一个被pygame允许调用的surface对象
mapsurfWidth = len(mapobj) * tilewidth
mapsurfHeight = (len(mapobj[0]) - 1) * tileheightrect + tileheight
mapSurf = pygame.Surface((mapsurfWidth,mapsurfHeight))
mapSurf.fill(BGCOLOR)
for x in range(len(mapobj)):
for y in range(len(mapobj[x])):
spaceRect = pygame.Rect((y * tilewidth, x * tileheightrect, tilewidth, tileheight))#设置每一个图片的位置
goldrect=pygame.Rect((y * tilewidth, x * tileheightrect + 10, tilewidth, tileheight))#设置金币的位置
#首先显示地图中地板和墙,绘制基础地图
if mapobj[x][y] in tileimage:
baseTile = tileimage[mapobj[x][y]]
elif mapobj[x][y] in outsidedecorate:
baseTile = tileimage[' ']
elif mapobj[x][y] in GOLD:
baseTile = tileimage['o']
elif mapobj[x][y] == '*':
baseTile = tileimage['o']
mapSurf.blit(baseTile , spaceRect)
#在挨个绘制树木、金币、终点和人物
if mapobj[x][y] in outsidedecorate:
mapSurf.blit(outsidedecorate[mapobj[x][y]], spaceRect)
elif mapobj[x][y] in GOLD:
if goldnum==0:
g0 = goldangel(GOLD['g'],y * tilewidth,x * tileheightrect + 10)
g0.blit(mapSurf)
#mapSurf.blit(GOLD['g'], goldrect)
elif goldnum==1:
g45 = goldangel(GOLD['g45'], y * tilewidth, x * tileheightrect + 10)
g45.blit(mapSurf)
#mapSurf.blit(GOLD['g45'], goldrect)
elif goldnum==2:
g90 = goldangel(GOLD['g90'], y * tilewidth, x * tileheightrect + 10)
g90.blit(mapSurf)
#mapSurf.blit(GOLD['g90'], goldrect)
elif goldnum==3:
g135 = goldangel(GOLD['g135'], y * tilewidth, x * tileheightrect + 10)
g135.blit(mapSurf)
#mapSurf.blit(GOLD['g135'], goldrect)
elif mapobj[x][y] == '*':
if goldnum == 0:
mapSurf.blit(GOLD['g'], goldrect)
elif goldnum == 1:
mapSurf.blit(GOLD['g45'], goldrect)
elif goldnum == 2:
mapSurf.blit(GOLD['g90'], goldrect)
if (x, y) == goal:
mapSurf.blit(locationimage['goal'], spaceRect)
if (x, y) == gamestateobj['player']:
mapSurf.blit(whoplayer[currentplayer], spaceRect)
return mapSurf
surface、blit方法不多说,就说两点,1、绘制地图的时候一定注意绘制的位置和先后顺序,2、绘制的图像的显示位置,弄明白它的位置究竟是相当于这个画板还是相对于整个屏幕而言的,和后期的屏幕是否能正常移动有大关系,毕竟一个屏幕是有可能无法装下一个大的完整的迷宫地图的。
##############
代码贴出来先,然后startmaze.py里面的图片地址需要替换为自己的图片存放路径,这样才可以运行。至于我为什么放在C盘是因为这样便于后面进行打包操作,适用于每一台用户名不相同的主机运行,这样编译器索引出来的地址才是相同的。
码云
效果图