python使用tkinter实现a星寻路可视化
- 运行结果
- A*寻路的核心公式是:F = G + H(我们的目标主要是计算F)
- G代表当前点走到下一个点需要的代价
- H代表下一个点到终点的距离
- F代表这一操作需要的总的代价
- A*寻路的两个核心列表
- open_list: 该列表用来存放当前需要计算的点
- close_list: 该列表用来存放过去已经计算过的点
- A*寻路核心变量解释
- base_point: 存放当前循环正在处理的基础点(每次循环处理都会让这个变量进行更新,且处理完后就把base_point这个变量追加进close_list列表里)
A*寻路的大致过程
循环概述
- 确定base_point
- 更新open_list列表
- 找到open_list列表代价F最小的点
- 把base_point追加进close_list列表里
- 下一次循环的base_point就是这次计算出来代价F最小的点
- 循环结束的条件是open_list为空或者已经找到了目标点(为空退出循环意味着没找到目标,坐标点被死路密封住了)
一步步流程的大致解释
- 起点:绿色的那个点(1,4)
- 目标点:黄色的星星(5,1)
- 开始寻路循环
- base_point现在代表的是起点(1,4)
- 以base_point生成A,B,C,D四个点,并把这四个点追加进open_list列表里,把起点放入close_list列表
- 此时
- open_list: [A点,B点,C点,D点]
- close_list:[起点,]
- 此时
- 以base_point生成A,B,C,D四个点,并把这四个点追加进open_list列表里,把起点放入close_list列表
- 开始计算open_list列表里的点的F(代价值)
- 因为 A.G:走到A点的代价G为1(这个G是自己定义的,我把每个格子走一步所花费的代价G均定义为1)
- 因为 A.H:A点到终点的距离为√20
- 两点间的距离公式:√((x1 - x2)² + (y1 - y2)²)
- 所以 A.F:G + H 得出F为√20 + 1
- 以此类推
- B.F : √34 + 1
- C.F : √32 + 1
- D.F : √18 + 1
- 该次处理结果得出D点的F是最小的,所以我们把D点当作下一次循环的base_point
- 第一轮循环结束了,开始第二轮操作
- 因为刚才得到D.F属于最小的,所以接下来就以D点为基础点生成下一步能走的点E(2,3)、F(2,5)这两个点,并把这两个点追加进open_list,然后把D点追加进close_list
- 此时
- base_point: D点
- open_list: [A点,B点,C点,E点,F点]
- close_list: [起点,D点]
- 此时
- 根据之前的计算规则计算出
- E.F:√13 + 1
- F.F:√25 + 1
- 此时
- open_list: [A点,B点,C点,E点,F点]
- 因为E点的总代价F在open_list里是最小的,下一次循环的base_point = E
-
第二轮循环结束了,开始第三轮操作
-
-
此时
- base_point: E点
- open_list: [A点,B点,C点,F点,G点]
- close_list: [起点,D点,E点]
-
G.F = √10 + 1,由此得到G是open_list里F最小的点
- 然后按照这个思路就得到
用python代码可视化a*寻路
-
A*寻路的核心代码AStar.py
-
GameManager.py负责可视化寻路(运行这个脚本,记住让其他脚本与这个脚本保持在同一个目录)
- 方向键控制主角移动
- 空格键刷新地图
- a按键往地图追加一个障碍物
-
MapPoint.py用来生成地图的单个点
-
FreePoint.py用来生成玩家或目标
-
GameMap.py用来生成整个地图
python代码
GameManager.py
import threading
import tkinter
import time
from AStar import a_star
from FreePoint import FreePoint
from GameMap import GameMap
from MapPoint import MapPoint
class GameManager:
def __init__(self):
self.win = None
self.canvas = None
self.point_type_dict = {
'player': ('circle', 'green'),
'obstacle': ('square', 'black'),
'AI': ('circle', 'yellow'),
'path': ('square', 'skyblue'),
}
self.player = None
self.AI = None
self.key_list = []
self.key_mapping = {
38: 'up',
40: 'down',
37: 'left',
39: 'right',
32: 'restore_map',
65: 'add_obstacle',
}
self.game_loop = True
self.path_list = []
self.width = 25
self.height = 25
self.game_map = GameMap(width=self.width, height=self.height)
def start(self):
# 创建窗口画背景
self.create_win()
self.draw_bg()
# 创建玩家和AI且画出来
self.player = FreePoint(x=22, y=9)
self.AI = FreePoint(x=0, y=0)
self.draw_point(self.point_type_dict['player'], self.player.get_coordinate(), 'player')
self.draw_point(self.point_type_dict['AI'], self.AI.get_coordinate(), 'AI')
# 初始化地图坐标讯息
self.game_map.restore_map()
# 生成随即障碍物
obstacle_count = 100
self.game_map.create_random_obstacle(self.get_occupied_points(), obstacle_count)
# 画障碍
self.draw_obstacle()
# 画路径
self.draw_path(self.AI.get_coordinate(), self.player.get_coordinate())
# 监听键盘消息
self.win.bind('<Key>', self.listen_key_board)
# 开启画面循环更新线程
t = threading.Thread(target=self.update_canvas)
t.setDaemon(True) # 设置为守护线程,也就退出程序时,关闭该线程
t.start()
self.win.protocol("WM_DELETE_WINDOW", self.close_game) # 关闭窗口时触发
# 窗口主循环
self.win.mainloop()
def create_win(self):
if not self.win:
self.win = tkinter.Tk()
self.win.wm_title("测试") # 窗口标题
self.win.geometry("1000x600") # 窗口大小
self.win.update()
# 画背景
def draw_bg(self):
if not self.canvas:
self.canvas = tkinter.Canvas(self.win, width=520, height=520, bg='pink')
self.canvas.place(x=int((self.win.winfo_width() - int(self.canvas.cget('width'))) // 2), y=int((self.win.winfo_height() - int(self.canvas.cget('height'))) // 2))
# 画格子,一共25 * 25个格子(20单位一格)
for x in range(self.width + 1):
self.canvas.create_line(10, x * 20 + 10, 510, x * 20 + 10) # 横线26条
self.canvas.create_line(x * 20 + 10, 10, x * 20 + 10, 510) #
# 根据坐标信息描绘点
def draw_point(self, point_type, coordinate, tag):
if point_type[0] == 'circle':
self.canvas.create_oval(10 + coordinate[0] * 20, 10 + coordinate[1] * 20, 30 + coordinate[0] * 20, 30 + coordinate[1] * 20, fill=point_type[1], tags=tag)
elif point_type[0] == 'square':
self.canvas.create_rectangle(10 + coordinate[0] * 20, 10 + coordinate[1] * 20, 30 + coordinate[0] * 20, 30 + coordinate[1] * 20, fill=point_type[1], tags=tag)
# 键盘监听
def listen_key_board(self, key):
self.key_list = []
self.key_list.append(self.key_mapping.get(key.keycode))
# 更新画布
def update_canvas(self):
while self.game_loop:
command = None
if self.key_list:
command = self.key_list.pop(0)
if command:
if command == 'restore_map':
# 清理已有路径和障碍
self.clear_path()
self.clear_obstacle()
# 还原地图数据
self.game_map.obstacle_set.clear()
self.game_map.restore_map()
# 重画障碍和路径
self.game_map.create_random_obstacle(self.get_occupied_points(), 100)
self.draw_obstacle()
self.draw_path(self.AI.get_coordinate(), self.player.get_coordinate())
elif command == 'add_obstacle':
self.clear_path()
# 新增一个随机障碍
point = self.game_map.create_random_obstacle(self.get_occupied_points(), 1)
self.draw_point(self.point_type_dict['obstacle'], point, 'obstacle')
self.draw_path(self.AI.get_coordinate(), self.player.get_coordinate())
else:
check_point = self.player.check_move(command)
# 检测将要移动的点是否是障碍物或者是否出界
if check_point not in self.game_map.obstacle_set and not self.check_out(check_point):
self.player.move(command)
self.canvas.delete("player")
self.draw_point(self.point_type_dict['player'], self.player.get_coordinate(), 'player')
self.draw_path(self.AI.get_coordinate(), self.player.get_coordinate())
time.sleep(0.01)
# 获取占用点列表
def get_occupied_points(self):
occupied_points = (self.AI.get_coordinate(), self.player.get_coordinate())
return occupied_points
# 画障碍
def draw_obstacle(self):
for point in self.game_map.obstacle_set:
self.draw_point(self.point_type_dict['obstacle'], point, 'obstacle')
# 清除障碍
def clear_obstacle(self):
self.canvas.delete('obstacle')
# 获取最新路径
def draw_path(self, start, end):
# 检测起点和终点是否在地图
if start in self.game_map.map and end in self.game_map.map:
# 使用A星寻路获取路径
start_time = time.time()
result_path = a_star(start, end, self.game_map.map)
# 获取玩家对AI的寻路
result_path_two = a_star(end, start, self.game_map.map)
if len(result_path_two) < len(result_path):
result_path = result_path_two
# 若存在路径
if result_path:
self.path_list = result_path
self.canvas.delete('path')
for point in result_path[:-1]:
self.draw_point(self.point_type_dict['path'], point, 'path')
# 清除路径
def clear_path(self):
self.canvas.delete('path')
self.path_list = []
# 检测坐标点是否出界
def check_out(self, point):
return point[0] < 0 or point[0] >= self.width or point[1] < 0 or point[1] >= self.height
# 关闭程序
def close_game(self):
self.game_loop = False
self.win.destroy()
def main():
game_manager = GameManager()
game_manager.start()
if __name__ == '__main__':
main()
AStar.py
import copy
import random
def a_star(start, end, raw_map_point_dict, calculate_count=1):
# 计算次数
calculate_count = max(calculate_count, 1)
for _ in range(calculate_count):
pass
map_point_dict = copy.deepcopy(raw_map_point_dict)
# 建立open集合和close集合
open_dict = {
}
close_dict = {
}
# 初始化地图起点
map_point_dict[start].G = 0
map_point_dict[start].H = get_H(start, end)
map_point_dict[start].F = map_point_dict[start].G + map_point_dict[start].H
# 把起点加紧open集合里
open_dict[start] = map_point_dict[start].F
# open为空时退结束循环
is_find_end = False
while open_dict and not is_find_end:
# 找出F代价最小的作为基点
base_point = list(list(open_dict.items())[0])
''' 随机找法 '''
equal_list = []
for key, value in open_dict.items():
if value < base_point[1]:
base_point[0] = key
base_point[1] = value
equal_list = [base_point]
elif value == base_point[1]:
equal_list.append((key, value))
if equal_list:
random.shuffle(equal_list)
base_point = equal_list[0]
''' 固定顺序找法 '''
# for key, value in open_dict.items():
# if value < base_point[1]:
# base_point[0] = key
# base_point[1] = value
# print(base_point)
# 把选出来的基点从open集合里去掉
del open_dict[base_point[0]]
# 获取基点相邻点坐标
neighbor_list = []
neighbor_list.append(('up', (base_point[0][0], base_point[0][1] - 1)))
neighbor_list.append(('down', (base_point[0][0], base_point[0][1] + 1)))
neighbor_list.append(('left', (base_point[0][0] - 1, base_point[0][1])))
neighbor_list.append(('right', (base_point[0][0] + 1, base_point[0][1])))
# 遍历相邻点(即处理相邻点)
for key, value in neighbor_list:
# 超出限定值
if value[0] < 0 or value[0] > 24 or value[1] < 0 or value[1] > 24:
continue
# 判定为障碍物
if map_point_dict[value].is_obstacle:
continue
# 判定该点是否已经在close里,即已经计算过的点
if value in close_dict:
continue
# 判定该点是否已经在open里
elif value in open_dict:
new_G = map_point_dict[value].G + map_point_dict[value].neighbor_cost[key]
if map_point_dict[value].G > new_G:
map_point_dict[value].parent_point.append(base_point[0])
map_point_dict[value].G = new_G
map_point_dict[value].F = new_G + map_point_dict[value].H
else:
map_point_dict[value].parent_point.append(base_point[0])
map_point_dict[value].G = 0 + map_point_dict[value].neighbor_cost[key]
map_point_dict[value].H = get_H(value, end)
map_point_dict[value].F = map_point_dict[value].G + map_point_dict[value].H
open_dict[value] = map_point_dict[value].F
# 判定是否找到终点
if value == end:
is_find_end = True
close_dict[base_point[0]] = base_point[1]
if is_find_end:
result_path = get_path(start, end, map_point_dict)
return result_path
else:
return []
# 计算终点代价
def get_H(start, end):
# 计算两点间的距离
distance = ((start[0] - end[0]) ** 2 + (start[1] - end[1]) ** 2) ** 0.5
return distance
# 获取路径列表
def get_path(start, end, map_point_dict):
result_path = []
result_path.append(end)
current_point = map_point_dict[end].get_parent()
while current_point != start:
result_path.append(current_point)
current_point = map_point_dict[current_point].get_parent()
result_path.reverse()
return result_path
if __name__ == '__main__':
from MapPoint import MapPoint
# 建立地图点25 * 25 (0 ~ 24)
map_point_dict = {
}
for x in range(25):
for y in range(25):
map_point_dict[(x, y)] = MapPoint(x=x, y=y)
# 输入起点和终点
start = (0, 0)
end = (22, 9)
print(a_star(start, end, map_point_dict))
MapPoint.py
class MapPoint:
def __init__(self, **kwargs):
self.x = kwargs.get('x', 0)
self.y = kwargs.get('y', 0)
self.F = kwargs.get('F')
self.G = kwargs.get('G')
self.H = kwargs.get('H')
self.parent_point = []
self.is_obstacle = False
self.neighbor_cost = {
}
self.neighbor_cost['up'] = self.neighbor_cost['down'] = self.neighbor_cost['left'] = self.neighbor_cost['right'] = 1
self.neighbor_cost['ul'] = self.neighbor_cost['ur'] = self.neighbor_cost['dl'] = self.neighbor_cost['dr'] = 1.414
def set_neighbor_cost(self, direct, value):
self.neighbor_cost[direct] = value
def get_coordinate(self):
return self.x, self.y
def get_parent(self):
return self.parent_point.pop() if self.parent_point else None
if __name__ == '__main__':
# 建立地图点25 * 25 (0 ~ 24)
map_point_dict = {
}
for x in range(25):
for y in range(25):
map_point_dict[(x, y)] = MapPoint(x=x, y=y)
# print(map_point_dict)
GameMap.py
import copy
import random
from MapPoint import MapPoint
class GameMap:
def __init__(self, **kwargs):
self.width = kwargs.get('width', 0)
self.height = kwargs.get('height', 0)
self.raw_map = {
}
self.map = {
}
self.obstacle_set = set()
self.init_map()
def init_map(self):
for x in range(self.width):
for y in range(self.height):
self.raw_map[(x, y)] = MapPoint(x=x, y=y)
def restore_map(self):
self.map = copy.deepcopy(self.raw_map)
for point in self.obstacle_set:
self.map[point].is_obstacle = True
# self.obstacle_set.clear()
def create_random_obstacle(self, occupied_points, create_num):
map_list = []
for point in self.map.keys():
if point not in occupied_points and not self.map[point].is_obstacle:
map_list.append(point)
create_num = min(create_num, len(map_list))
for _ in range(create_num):
point = map_list.pop(random.choice(range(len(map_list))))
self.map[point].is_obstacle = True
self.obstacle_set.add(point)
return point
def clear_obstacle(self):
while self.obstacle_set:
self.map[self.obstacle_set.pop()].is_obstacle = False
def delete_obstacle(self, points):
for point in points:
self.map[point].is_obstacle = False
if point in self.obstacle_set:
self.obstacle_set.remove(point)
return self.obstacle_set
if __name__ == '__main__':
GameMap(width=10, height=10).restore_map()
FreePoint.py
class FreePoint:
def __init__(self, **kwargs):
self.x = kwargs.get('x', 0)
self.y = kwargs.get('y', 0)
self.step = kwargs.get('step', 1)
self.speed = kwargs.get('speed', 1)
def move(self, direct):
if direct == 'up':
self.y -= self.step * self.speed
elif direct == 'down':
self.y += self.step * self.speed
elif direct == 'left':
self.x -= self.step * self.speed
elif direct == 'right':
self.x += self.step * self.speed
def check_move(self, direct):
x = self.x
y = self.y
if direct == 'up':
y = self.y - self.step * self.speed
elif direct == 'down':
y = self.y + self.step * self.speed
elif direct == 'left':
x = self.x - self.step * self.speed
elif direct == 'right':
x = self.x + self.step * self.speed
return x, y
def set_speed(self, num):
self.speed = num
def get_coordinate(self):
return self.x, self.y
if __name__ == '__main__':
test_point = FreePoint(x=2, y=3, step=2, speed=4)
print((test_point.x, test_point.y), test_point.step, test_point.speed)
代码大致解释
- FreePoint.py
- x坐标
- y坐标
- step
- speed
- move()
- up: y - step * speed
- down: y + step * speed
- left: x - step * speed
- right: x + step * speed
- MapPoint.py
- x坐标
- y坐标
- F # F = G + H
- G
- H
- parent_point # 该点的父亲坐标点
- is_obstacle # 障碍物标识
- neighbor_cost # 邻近格子的代价
-
# 默认初始直线代价为1,斜线代价为1.414 neighbor_cost = {} neighbor_cost['up'] = neighbor_cost['down'] = neighbor_cost['left'] = neighbor_cost['right'] = 1 neighbor_cost['ul'] = neighbor_cost['ur'] = neighbor_cost['dl'] = neighbor_cost['dr'] = 1.414
- GameManager.py
- 生成窗口
- 在窗口里画背景
- 输入start和end且实例化FreePoint
- 画点
- 监听键盘消息取得消息队列
- 画面更新循环
- 循环退出条件:游戏结束Flag
- pop出键盘消息队列
- 判断更新Flag
- True:
- FreePoint响应消息更新x, y
- 根据待画列表画出点
- 清除上一帧多余画点数据
- True:
- AStar.py
- 输入:起点 目标点 地图点信息
- 输出:A*寻路的路径坐标列表
总结
关于我写的这个A星算法还是可以考虑下进一步的优化。
我的文底有限,关于A*寻路的原理也可以看下这一篇文章A星寻论算法解读