python使用tkinter实现a星寻路可视化

python使用tkinter实现a星寻路可视化

  • 运行结果
    • 在这里插入图片描述
  • A*寻路的核心公式是:F = G + H(我们的目标主要是计算F)
    • G代表当前点走到下一个点需要的代价
    • H代表下一个点到终点的距离
    • F代表这一操作需要的总的代价
  • A*寻路的两个核心列表
    • open_list: 该列表用来存放当前需要计算的点
    • close_list: 该列表用来存放过去已经计算过的点
  • A*寻路核心变量解释
    • base_point: 存放当前循环正在处理的基础点(每次循环处理都会让这个变量进行更新,且处理完后就把base_point这个变量追加进close_list列表里)

A*寻路的大致过程

循环概述

  1. 确定base_point
  2. 更新open_list列表
  3. 找到open_list列表代价F最小的点
  4. 把base_point追加进close_list列表里
  5. 下一次循环的base_point就是这次计算出来代价F最小的点
  6. 循环结束的条件是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:[起点,]
  • 开始计算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
    1. 生成窗口
    2. 在窗口里画背景
    3. 输入start和end且实例化FreePoint
    4. 画点
    5. 监听键盘消息取得消息队列
    6. 画面更新循环
      • 循环退出条件:游戏结束Flag
      1. pop出键盘消息队列
      2. 判断更新Flag
        • True:
          • FreePoint响应消息更新x, y
          • 根据待画列表画出点
          • 清除上一帧多余画点数据
  • AStar.py
    • 输入:起点 目标点 地图点信息
    • 输出:A*寻路的路径坐标列表

总结

关于我写的这个A星算法还是可以考虑下进一步的优化。
我的文底有限,关于A*寻路的原理也可以看下这一篇文章A星寻论算法解读

猜你喜欢

转载自blog.csdn.net/Paper_Sheep/article/details/116044545