本文以Python作为服务器, Unity作为客户端引擎, 主要讨论的是服务端实现寻路并由客户端表现架构下的程序设计思路.
1. 整体思路
首先, 我们讨论寻路算法的比较和选择: A-star一般来说有更好的性能, 因为它是从起点到终点路径上的点与起点之间的最短路径计算, 而Dijkstra算法则是起点到图结构中所有点的最短路径计算, 有一些浪费了.
服务端寻路架构下, 服务端的作用在于定时根据游戏中玩家的位置变化, 进行新路线计算并把结果用point list的形式发送给客户端.
客户端的职责, 则是要把point list实际转化为画面上怪物单位的连续移动.
2. 讨论寻路算法
寻路算法, 指的是找出Start点到End点之间的一条最短路径. 这条最短路径通过的是地图上的可通行区域, 即应当绕开堵塞区域(block area).
我们可以从时间开销方面去比较这俩算法:
以常见的二叉堆(又名Priority Queue)实现为例, Dijkstra算法的时间开销为O(ElgV), E是边的数目, V是结点的数目, 算法最耗时的操作是从Q中提出V次的最小key值结点(extractMin), 和对所有E条边进行的decreaseKey操作.
值得注意的是: 迪杰斯特拉算法能够找出从S(Source)点到所有其他图结构中结点的最短路径.
Astar算法, 如果也是用二叉堆实现的话, 时间开销也是O(ElgV), 因为其最耗时操作也是Q中进行V次的extractMin操作, 以及对所有E条边进行的decreaseKey操作. 但是, 这里所说的都是算法的最坏情况, 也就是说找出来的最短路径需要遍历整个地图的最坏情况.
由于Astar算法是一个贪心算法(F = G+H中的H数值是用曼哈顿距离估算的, 并不是一个精确可靠的值), 因此虽然Dijkstra和Astar在二叉堆实现情况下都是O(ElgV), 大多数情况下Astar算法的时间开销只会明显少于Dijkstra, 我个人粗略估计至少是少一个数量级, 也就是说不到1/10, 特别是地图越大这个比例会越小.
用更直白的话来说, 迪杰斯特拉算法是找出S点到所有图中点的最短路径, A-star只找出S到E和S到路径上其他点的最短路径, 明显A-star要完成的任务更少. 由于游戏中我们大多数情况下只需要S到E点的最短路径, 因此A-star是更加省时节约开销的算法.
3. A-star with heap的实现
# encoding:utf-8
import time
from heapq import heappush, heappop
"""
path finding module: A* path finding算法, 用heap实现版本
quick guide:
start = Node(None, 20, 20)
end = Node(None, 50, 30)
print find_path(start, end) # return path list
It reads a map in the beginning of this script. Currently layout_medium.txt is chosen.
To download map(layout_medium.txt):
https://share.weiyun.com/5mtyHYi
"""
_2dmap = []
map_border = ()
g_close_list = {}
class Node:
def __init__(self, father, x, y, end=None):
if x < 0 or x >= map_border[0] or y < 0 or y >= map_border[1]:
raise Exception("node position can't beyond the border!")
self.father = father
self.x = x
self.y = y
if father != None:
G2father = calc_G(father, self)
if not G2father:
raise Exception("father is not valid!")
self.G = G2father + father.G
self.H = calc_H(self, end)
self.F = self.G + self.H # 初始化的时候计算出F
else: # it has no father, thus it is the start point
self.G = 0
self.H = 0
self.F = 0
def reset_father(self, father, new_G):
if father != None:
self.G = new_G
self.F = self.G + self.H
self.father = father
def calc_G(node1, node2): # 该点
dx = abs(node1.x - node2.x)
dy = abs(node1.y - node2.y)
if dx == 1 and dy == 0:
return 10 # same row
if dx == 0 and dy == 1:
return 10 # same col
if dx == 1 and dy == 1:
return 14 # cross
else:
return 0
def calc_H(cur, end): # 估算该点到终点距离(忽略墙等阻挡物), 采用Manhattan distance
return 10*(abs(end.x - cur.x) + abs(end.y - cur.y))
def addAdjacentIntoOpen_new(heap, open_list, close_list, node, end):
# 将该节点从开放列表移到关闭列表当中。
open_list.pop((node.x, node.y)) # key 为(x, y)形式的坐标tuple
close_list[(node.x, node.y)] = node
_adjacent = []
# 地图的layout的边界需要用0进行标记, 否则会进入except
try:
#_adjacent.append(Node(node, node.x - 1, node.y - 1, end)) # 这个时候就初始化了F值
_adjacent.append(Node(node, node.x, node.y - 1, end))
#_adjacent.append(Node(node, node.x + 1, no