上一篇文章主要完成了游戏地图的创建,其中简单为游戏寻路做了一点准备,就是格栅背景地图。游戏场景中的各种山川、河流、楼阁等都都可以视为为某种运动精灵的障碍物,那么精灵如何绕过障碍物运动都自己的目的地呢?目前关于寻路的算法有很多,最常见的就是A-Star算法,也叫A星算法。
典型的A-star算法处理的是运动物体和格栅大小一致,如果运动物体比格栅小,自然没有任何问题。如果运动物体比格栅大,典型的A-star算法就不能处理了。那么是不是A-star方法就不能处理了呢?不是,针对这个问题,国外有人提出了叫做 Brushfire的算法。据说得google才能找到。我简单看了一下,应该也是可以实现的。有兴趣的朋友可以自行探索。
典型的Astar算法网上有很多详细的讲解,有一位大拿的文章请参考,还有一些前人给出了C/C++/JAVA/JSP等各种语言的实现。这里就不再重复了。
本文解决的问题是大于格栅大小的物体的寻路问题,并尽可能实现任何大小的物体寻路。下面有几个示意图可以先看一下。



为了方便理解,这里用虚线展示精灵的运动路径,实际上是精灵的左上角运动轨迹。有些Astar算法只允许精灵在水平和垂直方向运动,有些可以8个方向(45度角)。这其实没有本质的区别,只是在寻路过程中对合格邻接点的筛选策略不同而已。完全可以根据自己的需要来处理。
好了,现在开始展示A-star算法。
既然涉及到格栅,那么格栅怎么描述呢?就是点的集合。先描述点:

class Node:def __init__(self, point, target, father=None):self.point = pointself.father = fatherif father is not None:self.gCost = father.gCost + self.get_gCost(father.point)self.hCost = self.get_hCost(target)self.fCost = self.gCost + self.hCostelse:self.gCost = 0self.hCost = 0self.fCost = 0# 计算自己到父节点的gCost。如果到父节点为 45 度方向,设置 gCost 为 14,否则设为 10def get_gCost(self, father):if abs(self.point[0] - father[0]) + abs(self.point[1] - father[1]) == 2:return 14return 10def get_hCost(self, target):return (abs(target[0] - self.point[0]) + abs(target[1] - self.point[1])) * 10def reset_father(self, father, newgCost):if father is not None:self.gCost = newgCostself.fCost = self.gCost + self.hCostself.father = father

关于cost的设置方法,有很多策略,有兴趣的可以进一步研究。这里使用最简单的实现,便于理解。
point是什么呢?就是格栅点的(X轴索引,Y轴索引),一个Tuple。
有了Node,下面就是A-Star了。
类属性定义:

class AStar:# stat and target 是Tuple (x, y), 是格栅的索引点,不是像素坐标。# size 为寻径主体的大小,表示边长为格栅尺寸的倍数# 对于非正方形主体,将其边长大的边计算size就可以def __init__(self, grids, start, target, size):self.grids = gridsself.size = sizeself.start = self.adjust_position(start)self.target = self.adjust_position(target)self.startNode = Node(self.start, self.target, None)self.targetNode = Node(self.target, self.target, None)# 开放列表和封闭列表使用集合,避免重复.# openSet 和 closeSet 字典结构,key 为 point, value 为 nodeself.openSet = dict()self.closeSet = dict()# 存储从起点到终点的路径, 逆序则为回程路径。注意,路径包含起点和终点。# 路径简化后包含起点、拐弯点和目的点。self.path = []def init_set(self):self.openSet.clear()self.closeSet.clear()

大家看到self.start 和 self.target,为什么没有直接使用精灵给出的起点和终点呢?因为很多时候玩家给精灵的目的点有时候可能是不可达的,也就是障碍物,玩家可能要求精灵去给定目的地附近的一个区域。起点位置呢?如果精灵是自动生产的,初始的位置有可能在障碍物里面,那么精灵自己是无法走出障碍物的,就需要调整精灵的起点位置。
那么如何调整呢?有两种策略,最简单的策略就是将精灵的起点和目的地调整为给定位置附近最近的一个可达点(合格的邻接点),另一种策略就是爱咋地咋地,精灵找不到路就呆在原地好了。看你的需要了,还有一种可能,调整范围不够大时,如论如何调整都不可达,被障碍物包围分离了,那就不在本文讨论范围了,重新设计你的地图吧。

    def find_min_fCost_node(self):# 在 当前路径中寻找代价最低节点作为寻找下一个节点的父节点。_min = 99999_point = self.startfor point, node in self.openSet.items():if _min > node.fCost:_min = node.fCost_point = pointreturn self.openSet[_point]def add_adjacent_into_openSet(self, node):self.openSet.pop(node.point)self.closeSet[node.point] = nodeadjacentNodeList = []# 对自己左上角周边8个点进行处理,如果物体能运动到新的位置,那么就添加为邻接点for i in [0, -1, 1]:for j in [0, -1, 1]:if i == 0 and j == 0:continue_adjacent = (node.point[0] + i, node.point[1] + j)if self.adjacent_is_through(_adjacent):adjacentNodeList.append(Node(_adjacent, self.target, node))# 检查每一个相邻的点# 如果斜向运动, gCost 设为14, 非斜向运动设置为 10for adjacent in adjacentNodeList:# 如果邻接点是终点,找到路径。if adjacent.point == self.target:gCost = node.gCost + adjacent.get_gCost(node.point)self.targetNode.reset_father(node, gCost)return Trueif adjacent.point in self.closeSet.keys():continue# 如果邻接点不在开放列表中,那么添加到开放列表中if adjacent.point not in self.openSet.keys():self.openSet[adjacent.point] = adjacent# 如果已经在开放列表中,那么通过 gCost判断这个点是否更近else:existNode = self.openSet[adjacent.point]if node.gCost + existNode.get_gCost(node.point) < existNode.gCost:existNode.reset_father(node, node.gCost + existNode.get_gCost(node.point))return False# 精灵调用looking_for_path() 后直接可以通过 self.path 获得最终路径def find_my_path(self):# 将起点加入到 opensetself.openSet[self.start] = self.startNodenode = self.startNodetry:while not self.add_adjacent_into_openSet(node):node = self.find_min_fCost_node()except BaseException as err:# 路径找不到print("No path from {} to {} with error: {}".format(self.start, self.target, err))return Falsereturn True

上面时传统的A-Star算法,在add_adjacent_into_openSet方法中使用了 adjacent_is_through()来判断是否该邻接点时可通过的。典型的判断方法是 是否改点是障碍物。为了解决大物体的寻路,只需要增加寻路过程中对邻接点的资格审查就可以了。

    # 这样可以对任何尺寸的运动物体寻路def adjacent_is_through(self, adjacent):# 判断运动物体移动到邻接点时所占用的区域是否出界,先判断左上角if not is_in_rect(adjacent, (0, 0), (GRID_X_QTY - 1, GRID_Y_QTY - 1)):return False# 如果运动物体超过一个格栅,判断右下角是否出界if self.size > 1:if not is_in_rect((adjacent[0] + self.size - 1, adjacent[1] + self.size - 1),(0, 0), (GRID_X_QTY - 1, GRID_Y_QTY - 1)):return False# 运动物体移动过到邻接点时所占用的区域有没有障碍物for i in range(0, self.size):if OBSTRUCT in self.grids[adjacent[1] + i][adjacent[0]: adjacent[0] + self.size]:return Falsereturn True# 如果给定的目的地不是一个合格的邻接点(不可达),那么调整目的地位置,找一个离当前目的地最近可可达点def adjust_position(self, position):if self.adjacent_is_through(position):return position_newPosition = position_distance = 9999# 调整范围可以设置为常量,大小根据实际情况设置就可以了。for i in range(-5, 6):for j in range(-4, 5):if i == 0 and j == 0:continueif self.adjacent_is_through((position[0] + i, position[1] + j)):if abs(i) + abs(j) < _distance:_newPosition = (position[0] + i, position[1] + j)_distance = abs(i) + abs(j)if _newPosition != position:print("Adjust position from {} to {}".format(position, _newPosition))return _newPositionraise Exception("I can't adjust the position!")def find_my_path(self):# 将起点加入到 opensetself.openSet[self.start] = self.startNodenode = self.startNodetry:while not self.add_adjacent_into_openSet(node):node = self.find_min_fCost_node()except BaseException as err:# 路径找不到print("No path from {} to {} with error: {}".format(self.start, self.target, err))return Falsereturn True

上面已经找到路径了,下面需要将路径整理出来。

    def mark_path(self):# 只有startNode的父节点为None。 这里从目标节点回溯直到起始节点,路径包含起点。# 由于是回溯,使用append还需使用 reverse 来反序,因此直接用 insert,插入到列表的最前端,不需要再反序。两种方法都可以。self.path.clear()# 将目的节点放在路径中node = self.targetNodeself.path.insert(0, node.point)# 根据下一个节点的位置while node.father is not None:self.path.insert(0, node.father.point)node = node.father

mark_path()方法将路径经过的所有节点(节点之间的距离最大为一个格栅),但是路径中如果一个方向上超过一个格栅距离呢?那么去掉直线子路径中的中间节点,只保留拐弯点就可以了。下面的方法将路径简化。

    def get_simplified_path(self):assert self.path.__len__() >= 2if self.path.__len__() == 2:returnnodesCount = self.path.__len__()tobeRemoved = []for m in range(2, nodesCount):# 如果相邻的三个节点在一条直线上,去掉中间节点if self.path[m][0] - self.path[m - 1][0] == self.path[m - 1][0] - self.path[m - 2][0] \and self.path[m][1] - self.path[m - 1][1] == self.path[m - 1][1] - self.path[m - 2][1]:tobeRemoved.insert(0, m-1)for m in tobeRemoved:self.path.pop(m)

上面基本完成了A-Star的类封装,精灵使用AStar时,尽可能少的知道类的内部实现,提供一个方法给外部调用就好了。

    def get_path(self):if self.find_my_path():self.mark_path()self.get_simplified_path()self.init_set()return Truereturn False

好了,AStar类的实现就是这样了,外面这么调用。如果精灵寻路:

 aStarPath = AStar(myGrids.grids, start, self.target, self.size)if aStarPath.get_path():self.path = aStarPath.path.copy()

上面AStar的第一个参数是格栅,那么格栅呢?

class Grids:def __init__(self):self.map = myMapself.grids = [[1] * GRID_X_QTY for i in range(GRID_Y_QTY)]# 根据障碍精灵产生矩阵def create_grids_from_map(self):for sp in self.map.elements:# 先计算该障碍物的起始位置row = sp.rect.top // GRID_HEIGHTcol = sp.rect.left // GRID_WIDTH# 根据障碍物的大小修改格栅for i in range(sp.rect[3] // GRID_HEIGHT):for j in range(sp.rect[2] // GRID_WIDTH):if col + j < GRID_X_QTY and row + i < GRID_Y_QTY:self.grids[row + i][col + j] = OBSTRUCTmyGrids = Grids()

格栅是基于地图的,myMap就是你创建的地图,上一篇文章中已经实现了。Grids类只是将地图抽象化为格栅。
myGrids = Grids() 只是实现最简单的单例类实现,如果游戏中多有的山川河流阁楼对所有精灵都是对等的,也就是都是障碍物,那么整个游戏只需要一个Grids对象了。对于一些大一点的游戏,比如人不能在河里走,但是船可以,那么就不能用单例模式了,需要对同一张地图,不同类型的精灵实现不同的格栅了,这时候需要在地图元素Element上再增加一个属性来区分对某个或某些精灵是否为障碍物了。有兴趣的朋友可以进一步实现。
另外,仔细观察上面的路径图片的朋友可能发现,有一些斜线路径视乎穿过了障碍物的一个小角。其实这不是问题,只是本文简化了处理,格栅点代表一个格栅是否为障碍物,可以将格栅的中心作为格栅点,那么实际上精灵的左上角在格栅点位置的右上方。当然,如果你不希望这样,只需要更严格的限制,需要进一步审查邻接点的资格,也就是adjacent_is_through()方法就可以了。
本格栅大小为20px,GRID_X_QTY/GRID_Y_QTY 分别为60/40。小精灵与单个格栅大小相同,大精灵为4个格栅大小的正方形。对于长方形精灵,比如车辆,那么寻路算法时可以设置为大小为长边的正方形格栅,处理动画时设计好相关spritesheet表就可以。
下一篇将基于AStar算法,给出精灵寻路中的动画效果。遇到敌人时的处理、英雄自动寻敌、敌人自动行走等一些列的处理。
本人初学python,望请大家批评指正。谢谢!

Python游戏之运动物体寻路——A星算法与扩展相关推荐

  1. cocos2d-x游戏实例(5)-A星算法(1)

    小满(bill man)个人原创,欢迎转载,转载请注明地址,小满(bill man)的专栏地址http://blog.csdn.net/bill_man 继续上一篇地图上的处理,不过和本篇相比,我们之 ...

  2. cocos2d-x游戏实例(8)-A星算法(4)

    小满(bill man)个人原创,欢迎转载,转载请注明地址,小满(bill man)的专栏地址http://blog.csdn.net/bill_man 继续A星算法,我们在经历了地图的检测,并且检测 ...

  3. cocos2d-x游戏实例(9)-A星算法(5)

    小满(bill man)个人原创,欢迎转载,转载请注明地址,小满(bill man)的专栏地址http://blog.csdn.net/bill_man 上一篇我们已经完成了A星算法,那么如何使用呢, ...

  4. cocos2d-x游戏实例(7)-A星算法(3)

    小满(bill man)个人原创,欢迎转载,转载请注明地址,小满(bill man)的专栏地址http://blog.csdn.net/bill_man 继续上一篇的内容,我们再看我们上一篇进行的部分 ...

  5. cocos2d-x游戏实例(6)-A星算法(2)

    小满(bill man)个人原创,欢迎转载,转载请注明地址,小满(bill man)的专栏地址http://blog.csdn.net/bill_man 上一篇中我们研究了A星算法的基本概念,本篇介绍 ...

  6. c++完整实现地图寻路A星算法

    A星寻路算法的讲解有很多,这里不再论述,只给出实现程序. AStar.h #pragma once#include <vector> #include <map> #inclu ...

  7. erlang实现A星算法

    A星算法是寻路的经典算法,在很多游戏中有使用,性能关键在于启发函数的设计. 函数式语言erlang实现一些算法上还是相当绕,有一些无可避免的地方需要用一些非典型的语法实现(变量不可变.无循环语法.无法 ...

  8. 传统运动物体检测方法的Python实现

    传统运动物体检测方法的Python实现 文章目录 传统运动物体检测方法的Python实现 一.目标跟踪算法综述 1. 传统方法:特征提取+滤波类搜索算法 2. 深度学习方法: 目标检测和相似度匹配 二 ...

  9. python+OpenCV笔记(三十七):检测运动物体——使用MOG/KNN背景差分器

    目录 一.基本背景差分器 二.MOG背景差分器 流程 代码编写 三.KNN背景差分器 目前,许多运动检测技术都是基于简单的背景差分概念的,即假设摄像头(视频)的曝光和场景中的光照条件是稳定的,当摄像头 ...

  10. python运动物体检测_运动检测ViBe算法python实现代码

    运动物体检测一般分为背景建模和运动物体分析两步.即构建不包含运动物体的背景模型.然后将新的视频帧和背景模型对比,找出其中的运动物体.目前比较好的背景建模算法有两种:1)文章(Zivkovic Z. ( ...

最新文章

  1. View Horizon Mirage安装手册(一)——Horizon Mirage介绍
  2. (C++)函数参数传递中的一级指针和二级指针
  3. html中引入css样式表的三种方式,css引用的几种方式是什么?
  4. 说道说道 ios 图片尺寸的问题
  5. 通向财务自由之路09_知道何时收手:如何保护你的资本
  6. ext4.0.7 样式兼容性
  7. 笔记:《深入浅出统计学》第十四章:卡方分布
  8. 机器学习的十大经典算法,面试必问
  9. BeanUtils与PropertyUtils的copyProperties方法的差别
  10. my android机器人作文,机器人作文400字
  11. vue Fullcalendar鼠标放上展示悬浮框 (tippyjs插件的简单使用)
  12. 记账软件如何查看收入支出
  13. 为什么计算机会出现两个用户,Win7登录时有2个账户|为什么电脑开机时有二个账户...
  14. skywalking源码分析第十八篇一agent端Trace三部曲一Span栈桢机制
  15. python实现有道词典翻译
  16. 基于面向对象的ATM自动取款机(未完善)
  17. android 编译luajit,【最新最全】为 iOS 和 Android 的真机和模拟器编译 Luajit 库
  18. 微信小程序NFC读写(我使用的是Ndef 标签其他的不会那个指令)
  19. 最佳编程笔记本_2020年如何选择最佳笔记本电脑进行编程?
  20. Oracle期末考试复习

热门文章

  1. 菜花这样做,就着它吃光三碗饭
  2. 【愚公系列】2022年07月 Go教学课程 004-Go代码注释
  3. 阿古斯机器人_7.3.2暗牧神器燃烧王座语音
  4. 电子元器件识别(图解)
  5. 十余种无限流量卡大对比,这一种的最实惠!
  6. 设计之星 ai_漫画 |《钢铁之星》:AI肆虐的时代,夹杂人类阴谋
  7. 任天堂游戏 html5,明年的预备阵容!任天堂承诺却还没出的作品
  8. @RequestHeader
  9. 微软宣布任命前MOTO亚太总裁梁念坚为中国区CEO
  10. win7共享无法关闭密码保护解决方法