迷宫小游戏 – 开发记录
- 开发
- 2026-01-29
- 202热度
- 0评论
一
一直对算法相关的东西很感兴趣,又一直想做点有意思的多人小游戏出来玩玩,在一次和朋友玩4399上的小游戏的时候,突然想到我现在可以自己试试仿造这些玩法做一个自己的多人小游戏出来玩玩,于是选定了经典的《森林冰火人》这款双人闯关小游戏作为参考对象。在经过思考琢磨之后,决定先做一个迷宫出来,类似坦克动荡游戏中的迷宫,其中就会包括我感兴趣的部分:迷宫生成算法。
二
开动!
直接打开PyCharm、新建项目、新建python3.12虚拟环境、向git仓库提交空白代码初始化()......
首先导入库:当然是我们大名鼎鼎的
```pygame```库啦!
import pygame # pygame库
import sys # 系统操作库,用于后面正常退出程序
import random # 随机功能库,生成随机迷宫地图的时候用
然后写一下固定的模板...
def main():
pygame.init() # pygame初始化
screen = pygame.display.set_mode((1200, 800)) # 设置窗口大小为1200*800像素
pygame.display.set_caption("迷宫") # 设置窗口标题为“迷宫”
while True: # 游戏主循环
for event in pygame.event.get(): # 获取输入事件
if event.type == pygame.QUIT: # 关闭窗口
pygame.quit() # 退出pygame
sys.exit() # 退出程序
screen.fill((255, 255, 255)) # 把整个窗口背景填充为白色(RGB值三个255)
pygame.display.update() # 更新窗口
if __name__ == '__main__': # 程序入口
main()
就这样运行一下,我们就能得到一个纯白色的游戏窗口了!(太简单了不放图了( ̄y▽, ̄)╭ )
接下来考虑到我们会有很多可以设置的值,我们可以新建一个设置类来保存我们的设置。比如窗口大小、背景颜色什么的,肯定不能直接写死在程序里,我们来把它们放在同一个地方统一管理。
顺便把迷宫每一个格子的大小、墙壁线的宽度、背景颜色、墙壁颜色什么的都写上。
class Settings:
def __init__(self):
self.block_size = 50 # 方格大小
self.screen_width = 1200 # 屏幕宽度(必须是方格大小的整数倍)
self.screen_height = 800 # 屏幕高度(必须是方格大小的整数倍)
self.line_width = 4 # 墙壁线宽度
self.bg_color = (255, 255, 255) # 背景颜色(元组RGB值)
self.line_color = (0, 0, 0) # 墙壁颜色(元组RGB值)
然后把代码中的数字都改成这个变量:
def main():
pygame.init()
setting = Settings() # 设置类的实例化
screen = pygame.display.set_mode((setting.screen_width + setting.line_width/2, setting.screen_height + setting.line_width/2)) # 把原来写死的数字改成变量(这里让窗口多加了半个线宽,是为了让窗口最下面和最右边的边线也能被完整显示出来)
pygame.display.set_caption("迷宫")
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(setting.bg_color) # 把原来写死的数字改成变量
pygame.display.update()
这样看起来就规范多啦ヾ(≧▽≦*)o
迷宫地图的数据结构
为了方便开发,我们需要选择一个合适的数据结构来存储我们的迷宫地图。秉持着面向对象的开发思想,首先我们要先定义一个新的类来存储迷宫地图。为了表示格子和它与它相邻的格子的连接情况,我们可以用一个字典来表示它与上下左右的格子的连通情况:
{"u": False, "d": True, "l": True, "r": False}
其中
```u``` ```d``` ```l``` ```r```分别表示```上``` ```下``` ```左``` ```右```,```True```表示连通,```False```表示不连通。
例如上面这一行就表示这个格子和它左边和下边的格子是连通的,而和它上边和右边的格子是不连通的。
然后我们定义一个新的关卡地图类,在里面用数组套数组(即类似二维数组)来表示我们的排成矩阵的所有格子。
class Level:
def __init__(self, setting): # 初始化方法
self.line_width = setting.line_width # 将设置内容保存到自身中,下几行同理
self.width = setting.screen_width
self.height = setting.screen_height
self.block_size = setting.block_size
self.line_color = setting.line_color
self.bg_color = setting.bg_color
self.n = int(setting.screen_width / setting.block_size) # 根据窗口大小和格子大小计算格子行列数
self.m = int(setting.screen_height / setting.block_size)
self.blocks = [] # 用于存储所有格子的数组
for i in range(self.n): # 双重循环将每一个格子添加进数组
self.blocks.append([])
for j in range(int(self.m)):
self.blocks[i].append({"u": False, "d": False, "l": False, "r": False})
可以看到在我们的新的类中,属性
```n``` 和```m```用来记录有多少列格子、每列有多少个格子。把所有格子存在```blocks```中,这样我们就可以直接通过横纵坐标来访问任意一个格子了。例如:```blocks[i][j]```就表示从左往右第i+1列,从上往下第j+1行的格子了。
- 为什么要+1?因为我们的数组下标都是从0开始的。
-
为什么是从上往下?因为在电脑的坐标系统中,我们的坐标系默认左上角是原点,从左往右是x轴,从上往下是y轴。
在初始化的最后面我们用了双重循环来给这个二维数组中添加了我们每一个格子的初始状态。没错,每个格子的初始状态都是不连通任何格子的。至于为什么初始不连通,等到后面详解生成迷宫的算法你就明白了。
接下来我们还要把迷宫画出来。我们给
```Level```类添加一个新方法:
def draw(self, screen): # 绘制到屏幕上的方法
for i in range(int(self.n+1)): # 绘制所有竖线
pygame.draw.line(screen, self.line_color, (i*self.block_size, 0), (i*self.block_size, self.height), self.line_width)
for j in range(int(self.m+1)): # 绘制所有横线
pygame.draw.line(screen, self.line_color, (0, j*self.block_size), (self.width, j*self.block_size), self.line_width)
for i in range(len(self.blocks)): # 遍历每一个格子,把需要连通的地方的墙壁“拆”掉
for j in range(len(self.blocks[i])):
if self.blocks[i][j]["u"]:
pygame.draw.line(screen, self.bg_color, (i*self.block_size+self.line_width/2+1, j*self.block_size), (i*self.block_size+self.block_size-self.line_width/2, j*self.block_size), self.line_width)
if self.blocks[i][j]["l"]:
pygame.draw.line(screen, self.bg_color, (i*self.block_size, j*self.block_size+self.line_width/2+1), (i*self.block_size, j*self.block_size+self.block_size-self.line_width/2), self.line_width)
这个方法首先用两个循环把所有的竖线和横线都画了出来,然后用了一个双重循环遍历每一个格子,如果这个格子和左边的格子是连通的,就把他们之间填充上背景颜色,也就是相当于把墙“拆”了。上边同理。那为什么没有右边和下边呢?其实很简单:等我们轮到下边的格子的时候,如果下边的格子和这个格子是连通的,那么下边的格子就会把它们之间的墙给“拆”掉,不需要原来的格子处理。换言之:每个格子只负责自己上边和左边的墙壁,整个地图的可能需要处理的墙壁就已经被覆盖完了。
接下来在主函数中加入我们的类实例化的代码:
def main():
pygame.init()
setting = Settings()
screen = pygame.display.set_mode((setting.screen_width + setting.line_width/2, setting.screen_height + setting.line_width/2))
pygame.display.set_caption("迷宫")
l = Level(setting) # 关卡地图类的实例化
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(setting.bg_color) # 填充背景
l.draw(screen) # 绘制地图
pygame.display.update()
然后运行看看效果:
这就是我们想要的效果。
什么?你说你想测试一下“拆”墙功能?好的,我们可以添加一下测试代码,手动把一个格子的连通性改为
```Ture```:
def main():
pygame.init()
setting = Settings()
screen = pygame.display.set_mode(
(setting.screen_width + setting.line_width / 2, setting.screen_height + setting.line_width / 2))
pygame.display.set_caption("迷宫")
l = Level(setting)
l.blocks[10][10]["u"] = True # 手动添加这个格子和上边连通
l.blocks[10][10]["l"] = True # 手动添加这个格子和左边连通
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(setting.bg_color)
l.draw(screen)
pygame.display.update()
然后运行看看效果:
看见了吗?我们的这个格子和左边和上边都连通了,说明我们的代码没有问题ψ(`∇´)ψ
随机生成迷宫地图的算法
上网搜索查阅一番信息,在这篇文章中很形象的用动画展示了各种迷宫生成算法:四种迷宫生成算法的实现和可视化_四种迷宫生成算法和迷宫寻路算法-CSDN博客,在了解到经典迷宫其实可以当做一棵树来看之后,我最终决定采用文中的
```随机化`Kruskal`算法```来生成迷宫,也是最小生成树的其中一个算法。
(关于这个算法的详细讲解可以看看这一篇文章:最小生成树详解(模板 + 例题)-CSDN博客)
接下来就用Python代码来实现这个算法吧!
首先我们给所有格子按照一定的顺序编号,如下图所示:
(根据坐标计算格子编号是一件很简单的事情,应该不用我解释了吧(✿◡‿◡))
然后我们在关卡地图类中先写下生成随机迷宫地图的方法:
def generate_maze(self):
然后我们在这里面先写下
```查并集```的代码:
(对于查并集算法不了解的可以看看这个:算法学习笔记(1) : 并查集 - 知乎)
def generate_maze(self):
fa = {} # 父节点记录
def connect_block(x, y): # 连接两个点到同一个集合中
fa[findfa(x)] = findfa(y)
def findfa(x): # 用于递归寻找最终父节点,方便判断两个点是否在同一个集合中
if fa[x] == x:
return x
return findfa(fa[x])
for i in range(self.m): # 用双重循环初始化所有点的父节点都是自己
for j in range(self.n):
fa[i*self.n+j] = i*self.n+j # 这个i*n+j就是用坐标算编号的方法,你猜对了吗?
接着就可以在后面跟着写下最小生成树的算法:
edges = [] # 用于存储所有边的列表
for i in range(self.m): # 双重循环把所有边都加进列表
for j in range(self.n):
if j != self.n-1: # 最后一列点不需要加右边的边,故n-1
edges.append((i*self.n+j, i*self.n+j+1)) # 把这个点和它右边的点之间的边加进去
if i != self.m-1: # 最下面一行点不需要加下边的边,故m-1
edges.append((i*self.n+j, (i+1)*self.n+j)) # 把这个点和它下边的点之间的边加进去
cnt = 0 # 记录已经打通了多少边
while cnt < self.m*self.n-1: # 打通n*m-1个边就刚刚好是一颗树了
t = random.choice(edges) # 因为我们的边没有权重,随机选一条边就好,正好对应随机的迷宫地图
edges.remove(t) # 取出来的边就要从列表中删除
if findfa(t[0]) != findfa(t[1]): # 确认这两个点是不是已经连在一起了
connect_block(t[0], t[1]) # 连接这个边的两个点
cnt += 1 # 计数值加一
算法这样即可。我们发现被连接起来的两个点正好就是我们迷宫中要“拆”掉的墙,所以在查并集的算法中,我们可以直接在连接两个点的函数里面把迷宫的墙给“拆”了。
def connect_block(x, y):
fa[findfa(x)] = findfa(y)
x1 = x % self.n
y1 = x // self.n
x2 = y % self.n
y2 = y // self.n
if x1 == x2: # 在上面的算法部分后面直接顺便把格子的记录也更新了
if y1 > y2:
self.blocks[x1][y1]["u"] = True
self.blocks[x2][y2]["d"] = True
else:
self.blocks[x1][y1]["d"] = True
self.blocks[x2][y2]["u"] = True
else:
if x1 > x2:
self.blocks[x1][y1]["l"] = True
self.blocks[x2][y2]["r"] = True
else:
self.blocks[x1][y1]["r"] = True
self.blocks[x2][y2]["l"] = True
这个时候我们的关卡地图类的完整代码如下:
class Level:
def __init__(self, setting):
self.line_width = setting.line_width
self.width = setting.screen_width
self.height = setting.screen_height
self.block_size = setting.block_size
self.line_color = setting.line_color
self.bg_color = setting.bg_color
self.n = int(setting.screen_width / setting.block_size)
self.m = int(setting.screen_height / setting.block_size)
self.blocks = []
for i in range(self.n):
self.blocks.append([])
for j in range(int(self.m)):
self.blocks[i].append({"u": False, "d": False, "l": False, "r": False})
def draw(self, screen):
for i in range(int(self.n+1)):
pygame.draw.line(screen, self.line_color, (i*self.block_size, 0), (i*self.block_size, self.height), self.line_width)
for j in range(int(self.m+1)):
pygame.draw.line(screen, self.line_color, (0, j*self.block_size), (self.width, j*self.block_size), self.line_width)
for i in range(len(self.blocks)):
for j in range(len(self.blocks[i])):
if self.blocks[i][j]["u"]:
pygame.draw.line(screen, self.bg_color, (i*self.block_size+self.line_width/2+1, j*self.block_size), (i*self.block_size+self.block_size-self.line_width/2, j*self.block_size), self.line_width)
if self.blocks[i][j]["l"]:
pygame.draw.line(screen, self.bg_color, (i*self.block_size, j*self.block_size+self.line_width/2+1), (i*self.block_size, j*self.block_size+self.block_size-self.line_width/2), self.line_width)
def generate_maze(self):
fa = {}
def connect_block(x, y):
fa[findfa(x)] = findfa(y)
x1 = x % self.n
y1 = x // self.n
x2 = y % self.n
y2 = y // self.n
if x1 == x2:
if y1 > y2:
self.blocks[x1][y1]["u"] = True
self.blocks[x2][y2]["d"] = True
else:
self.blocks[x1][y1]["d"] = True
self.blocks[x2][y2]["u"] = True
else:
if x1 > x2:
self.blocks[x1][y1]["l"] = True
self.blocks[x2][y2]["r"] = True
else:
self.blocks[x1][y1]["r"] = True
self.blocks[x2][y2]["l"] = True
def findfa(x):
if fa[x] == x:
return x
return findfa(fa[x])
for i in range(self.m):
for j in range(self.n):
fa[i*self.n+j] = i*self.n+j
edges = []
for i in range(self.m):
for j in range(self.n):
if j != self.n-1:
edges.append((i*self.n+j, i*self.n+j+1))
if i != self.m-1:
edges.append((i*self.n+j, (i+1)*self.n+j))
cnt = 0
while cnt < self.m*self.n-1:
t = random.choice(edges)
edges.remove(t)
if findfa(t[0]) != findfa(t[1]):
connect_block(t[0], t[1])
cnt += 1
主函数完整代码如下:
def main():
pygame.init()
setting = Settings()
screen = pygame.display.set_mode(
(setting.screen_width + setting.line_width / 2, setting.screen_height + setting.line_width / 2))
pygame.display.set_caption("迷宫")
l = Level(setting) # 新增关卡地图类的实例化
l.generate_maze() # 调用方法生成迷宫地图
# l.blocks[10][10]["u"] = True # 手动添加这个格子和上边连通 # 这两行别忘了删掉
# l.blocks[10][10]["l"] = True # 手动添加这个格子和左边连通
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(setting.bg_color)
l.draw(screen) # 新增绘制地图
pygame.display.update()
我们再运行一下看看效果:
现在迷宫地图就成功生成出来啦!你可以重复运行几次,看看每次的迷宫地图应该都不一样。
添加玩家角色
这就很简单了(~ ̄▽ ̄)~
首先在设置类里面添加一点新的设置项:
class Settings:
def __init__(self):
self.block_size = 50
self.screen_width = 1200
self.screen_height = 800
self.line_width = 4
self.bg_color = (255, 255, 255)
self.line_color = (0, 0, 0)
self.player_default_color = (120, 0, 120) # 玩家默认颜色(元组RGB值)(这是紫色,你可以自己改成你喜欢的颜色)
self.player_default_r = 15 # 玩家默认半径
然后我们新建一个玩家类:
class Player:
def __init__(self, setting, x, y, color=None, r=None):
self.block_size = setting.block_size
self.color = color if color else setting.player_default_color
self.r = r if r else setting.player_default_r
self.x = x
self.y = y
def draw(self, screen): # 把玩家绘制到屏幕上(虽然只是一个球而已)
pygame.draw.circle(screen, self.color, (self.x*self.block_size+self.block_size/2, self.y*self.block_size+self.block_size/2), self.r)
def move(self, l, direction): # 移动方法
if direction == "U" and l.blocks[self.x][self.y]["u"]:
self.y -= 1
if direction == "D" and l.blocks[self.x][self.y]["d"]:
self.y += 1
if direction == "L" and l.blocks[self.x][self.y]["l"]:
self.x -= 1
if direction == "R" and l.blocks[self.x][self.y]["r"]:
self.x += 1
然后我们在主函数中添加玩家实例,并把按键绑定:
def main():
pygame.init()
setting = Settings()
screen = pygame.display.set_mode((setting.screen_width + setting.line_width/2, setting.screen_height + setting.line_width/2))
l = Level(setting)
l.generate_maze()
p1 = Player(setting, 0, 0) # 实例化玩家类
pygame.display.set_caption("迷宫")
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN: # 绑定移动按键(这里是WASD和方向键都可以)
if event.key == pygame.K_w or event.key == pygame.K_UP:
p1.move(l, "U")
if event.key == pygame.K_s or event.key == pygame.K_DOWN:
p1.move(l, "D")
if event.key == pygame.K_a or event.key == pygame.K_LEFT:
p1.move(l, "L")
if event.key == pygame.K_d or event.key == pygame.K_RIGHT:
p1.move(l, "R")
screen.fill(setting.bg_color)
l.draw(screen)
p1.draw(screen) # 绘制玩家,这个绘制顺序应当在地图后面
pygame.display.update()
运行看看效果:
玩家已经出现在屏幕上了!
这个时候试试按下或方向键来尝试操控移动,如果它按照预想的样子移动了,那我们的迷宫小游戏就算是完成啦(❁´◡`❁)
三
还有一点可以改进的地方,就是random库是可以指定随机种子的,只要种子一样,随机值都会一样,生成出来的迷宫地图也会说完全一样的,就像Minecraft游戏生成世界一样。我们同样可以在设置中加上种子选项。加上之后最终的全部完整代码如下:
import pygame
import sys
import random
class Settings:
def __init__(self):
self.block_size = 50
self.screen_width = 1200
self.screen_height = 800
self.line_width = 4
self.bg_color = (255, 255, 255)
self.line_color = (0, 0, 0)
self.player_default_color = (120, 0, 120)
self.player_default_r = 15
self.random_seed = 0 # 随机数种子(设置为0则每次运行都生成一个随机数)
class Level:
def __init__(self, setting):
self.line_width = setting.line_width
self.width = setting.screen_width
self.height = setting.screen_height
self.block_size = setting.block_size
self.line_color = setting.line_color
self.bg_color = setting.bg_color
self.n = int(setting.screen_width / setting.block_size)
self.m = int(setting.screen_height / setting.block_size)
self.random_seed = setting.random_seed if setting.random_seed else random.randint(1, 10000000000000000) # 如果设置种子为0,则随机生成一个种子
self.blocks = []
for i in range(self.n):
self.blocks.append([])
for j in range(int(self.m)):
self.blocks[i].append({"u": False, "d": False, "l": False, "r": False})
def draw(self, screen):
for i in range(int(self.n+1)):
pygame.draw.line(screen, self.line_color, (i*self.block_size, 0), (i*self.block_size, self.height), self.line_width)
for j in range(int(self.m+1)):
pygame.draw.line(screen, self.line_color, (0, j*self.block_size), (self.width, j*self.block_size), self.line_width)
for i in range(len(self.blocks)):
for j in range(len(self.blocks[i])):
if self.blocks[i][j]["u"]:
pygame.draw.line(screen, self.bg_color, (i*self.block_size+self.line_width/2+1, j*self.block_size), (i*self.block_size+self.block_size-self.line_width/2, j*self.block_size), self.line_width)
if self.blocks[i][j]["l"]:
pygame.draw.line(screen, self.bg_color, (i*self.block_size, j*self.block_size+self.line_width/2+1), (i*self.block_size, j*self.block_size+self.block_size-self.line_width/2), self.line_width)
def generate_maze(self):
fa = {}
def connect_block(x, y):
fa[findfa(x)] = findfa(y)
x1 = x % self.n
y1 = x // self.n
x2 = y % self.n
y2 = y // self.n
if x1 == x2:
if y1 > y2:
self.blocks[x1][y1]["u"] = True
self.blocks[x2][y2]["d"] = True
else:
self.blocks[x1][y1]["d"] = True
self.blocks[x2][y2]["u"] = True
else:
if x1 > x2:
self.blocks[x1][y1]["l"] = True
self.blocks[x2][y2]["r"] = True
else:
self.blocks[x1][y1]["r"] = True
self.blocks[x2][y2]["l"] = True
def findfa(x):
if fa[x] == x:
return x
return findfa(fa[x])
for i in range(self.m):
for j in range(self.n):
fa[i*self.n+j] = i*self.n+j
edges = []
for i in range(self.m):
for j in range(self.n):
if j != self.n-1:
edges.append((i*self.n+j, i*self.n+j+1))
if i != self.m-1:
edges.append((i*self.n+j, (i+1)*self.n+j))
cnt = 0
random.seed(self.random_seed) # 采用设置的随机种子
while cnt < self.m*self.n-1:
t = random.choice(edges)
edges.remove(t)
if findfa(t[0]) != findfa(t[1]):
connect_block(t[0], t[1])
cnt += 1
class Player:
def __init__(self, setting, x, y, color=None, r=None):
self.block_size = setting.block_size
self.color = color if color else setting.player_default_color
self.r = r if r else setting.player_default_r
self.x = x
self.y = y
def draw(self, screen):
pygame.draw.circle(screen, self.color, (self.x*self.block_size+self.block_size/2, self.y*self.block_size+self.block_size/2), self.r)
def move(self, l, direction):
if direction == "U" and l.blocks[self.x][self.y]["u"]:
self.y -= 1
if direction == "D" and l.blocks[self.x][self.y]["d"]:
self.y += 1
if direction == "L" and l.blocks[self.x][self.y]["l"]:
self.x -= 1
if direction == "R" and l.blocks[self.x][self.y]["r"]:
self.x += 1
def main():
pygame.init()
setting = Settings()
screen = pygame.display.set_mode((setting.screen_width + setting.line_width/2, setting.screen_height + setting.line_width/2))
l = Level(setting)
l.generate_maze()
p1 = Player(setting, 0, 0)
pygame.display.set_caption("迷宫")
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_w or event.key == pygame.K_UP:
p1.move(l, "U")
if event.key == pygame.K_s or event.key == pygame.K_DOWN:
p1.move(l, "D")
if event.key == pygame.K_a or event.key == pygame.K_LEFT:
p1.move(l, "L")
if event.key == pygame.K_d or event.key == pygame.K_RIGHT:
p1.move(l, "R")
screen.fill(setting.bg_color)
l.draw(screen)
p1.draw(screen)
pygame.display.update()
if __name__ == '__main__':
main()
我还计划在后期加入多人联机的功能,让大家一起玩,这可比一个人玩有意思多了!还能加入一些道具什么的......幻想一下,能够变成《坦克动荡》这样的游戏也不错(~ ̄▽ ̄)~
这个项目已经在github上共享了,快来给我点个star吧(✿◠‿◠):
KirkLee12345/maze: 一个简单的走迷宫小游戏,使用最小生成树算法来生成迷宫。
如果你想体验这个小游戏,可以直接在这个github仓库的release页面下载.exe程序,用Windows电脑直接打开就能玩了。如果你无法访问github,也可以点这里从我的云盘上下载:
欢迎大佬评论指正!





