设为首页收藏本站|繁體中文

Project1

 找回密码
 注册会员
搜索
查看: 4262|回复: 8
打印 上一主题 下一主题

[RMXP发布] 【算法】Dijkstra寻路算法在RM地图上的应用

[复制链接]

Lv4.逐梦者 (版主)

梦石
0
星屑
9532
在线时间
5073 小时
注册时间
2013-6-21
帖子
3580

开拓者贵宾剧作品鉴家

跳转到指定楼层
1
发表于 2014-11-27 19:54:56 | 只看该作者 回帖奖励 |正序浏览 |阅读模式

加入我们,或者,欢迎回来。

您需要 登录 才可以下载或查看,没有帐号?注册会员

x
本帖最后由 RyanBern 于 2014-12-12 17:31 编辑

在RM游戏中,事件寻路问题一直是一个热点话题。你是否还为移动角色而一个个数格子而烦恼?我想大多数人都会想,要是有这样一个命令,你告诉你的事件的目的地是哪里,那么只要有一条路通向那里,事件就会找出一条路自己走过去。这就是我们常说的“寻路脚本”。

如今,有各种各样是寻路脚本已经比较完美得解决了这个问题,例如著名的A星算法,这个算法可以说被广泛得用到各种游戏上(例如RTS游戏,ARPG游戏等),它依靠的原理不是很困难,效率也不错。当然还有myownroc写的BFS寻路脚本,利用广度优先搜索也可以解决问题。不过这个帖子里面介绍的是另一种寻路算法——Dijkstra算法。
(此帖的研究结果有些坑,虽然想法比较好,但是得到的结果让人比较失望)

对图论有接触的人对这个算法应该并不陌生,Dijkstra算法是计算图中一个点到其他所有点最短路径的一个算法,也是目前最好的最短路径算法之一,时间复杂度为O(n^2),空间复杂度为O(n)。于是在RM游戏中,一张地图就可以看作是一张有特殊结构的网格图,每个块最多只有4个邻居,并且它到它所有邻居的距离都相等。Dijkstra算法作为图论中最短路径的一般算法,当然可以用在RM的地图中。

Dijkstra算法的步骤是这样的。设V是所有顶点的集合,U里面是存放已经求出从起点到它最短路径的所有顶点,V-U是尚未确定最短路径的顶点集合。一开始U里面只有起点这个顶点。然后执行下面的步骤。

(1)在集合V-U中选取距离(注,此值需要额外记录)起点最小的顶点v,加入集合U
(2)对剩下V-U中各顶点的距离值进行修正,如果加入顶点v后,使得起点到V-U中的各个点距离变小,则修改距离值。

重复(1)(2)操作,直到所有与起点可达的顶点都在U中。

有关Dijkstra算法的正确性,这里不加多说,不过,保证Dijkstra算法正确的有一个条件希望大家记住,就是要求所有点之间的距离都是正数。

因此,结合(1)(2)两步,我们需要做下面的事情:
在(1)中,我们需要找到这个最小值,因此要对V-U顶点进行遍历。
在(2)中,我们需要对V-U中的点的距离进行修正,因此还要对V-U顶点再遍历一次。

假设地图的图块是n*n,那么我们的遍历次数会非常多,在这里我们利用RM地图的特点进行简化,可以极大降低复杂度。
RM地图特点为:每个点最多只有4个邻居,而且每个点到它邻居的距离均相等,这里不妨设为1

那么,(1)(2)两步的遍历空间就会极大减少。
由于每两个相邻顶点的距离都是1,进行修正时,修正过程中产生的值不会是最小的,因此最小值还是在原来U中顶点上达到。这就说明,先产生的可达顶点距离小,后产出的顶点距离大,而我们优先处理距离小的顶点,这是个First-Come-First-Served原则,因此可以抽象出来队列模型,我们就省去了(1)中的遍历。
为U中添加结点时,需要修正的点并不是地图上所有顶点,而时和新顶点相邻的所有顶点,而每个点的邻居最多4个,因此我们就省去了(2)中的遍历。

细心的人可以看到。这时,Dijkstra算法在RM地图中已经退化成了一个BFS算法(这就是我说的比较坑的地方,这么好的算法原来和BFS是一样的),执行步骤如下:
(1)将起点加入队列中
(2)从队列中取出第一个点v
(3)将v点所有可达的(还为进入队列一次的)邻居都放入到队列中,并记录路线方向
(4)如果队列不空,转(2),否则退出循环
(5)回溯路径,产生移动指令。

代码如下(以下算法可以在游戏中使用):
RUBY 代码复制
  1. =begin
  2. Dijkstra寻路算法 By RyanBern
  3.  
  4. 用法:在事件脚本中使用如下代码:
  5.  
  6. $game_player.dijkstra_move_to(x, y) #让主角移动到点(x, y)
  7.  
  8. $game_map.events[1].dijkstra_move_to(x, y) #让地图上的1号事件移动到点(x, y)
  9.  
  10. 注意,如果点(x, y)无法达到,那么角色将不会移动
  11. =end
  12. module Dijkstra
  13.   class Point
  14.     attr_accessor :x
  15.     attr_accessor :y
  16.     def initialize(x, y)
  17.       @x = x
  18.       @y = y
  19.     end
  20.   end
  21.   def generate_move_route(final_x, final_y)
  22.     route = []
  23.     path = Table.new($game_map.width, $game_map.height)
  24.     queue = []
  25.     queue << Point.new(self.x, self.y)
  26.     path[self.x, self.y] = -1
  27.     if !$game_map.valid?(final_x, final_y) || self.x == final_x && self.y == final_y
  28.       return route
  29.     end
  30.     found = false
  31.     while(!queue.empty? && !found)
  32.       point = queue.shift
  33.       [2,4,6,8].each do |d|
  34.         new_x = point.x + (d == 6 ? 1 : d == 4 ? -1 : 0)
  35.         new_y = point.y + (d == 2 ? 1 : d == 8 ? -1 : 0)
  36.         if self.passable?(point.x, point.y, d) && path[new_x, new_y] == 0
  37.           queue << Point.new(new_x, new_y)
  38.           path[new_x, new_y] = d
  39.           found = true if new_x == final_x && new_y == final_y
  40.         end
  41.       end
  42.     end
  43.     if found
  44.       x = final_x
  45.       y = final_y
  46.       while(path[x, y] != -1)
  47.         d = path[x, y]
  48.         route << d
  49.         x -= (d == 6 ? 1 : d == 4 ? -1 : 0)
  50.         y -= (d == 2 ? 1 : d == 8 ? -1 : 0)
  51.       end
  52.     end
  53.     return route
  54.   end
  55.   def generate_move_command(route)
  56.     list = Array.new(route.size){|i| RPG::MoveCommand.new(route[route.size - 1 - i] / 2)}
  57.     list << RPG::MoveCommand.new(0)
  58.     commands = RPG::MoveRoute.new
  59.     commands.skippable = false
  60.     commands.repeat = false
  61.     commands.list = list
  62.     return commands
  63.   end
  64.   def dijkstra_move_to(x, y)
  65.     route = generate_move_route(x, y)
  66.     commands = generate_move_command(route)
  67.     self.force_move_route(commands)
  68.   end
  69. end
  70.  
  71. class Game_Character
  72.   include Dijkstra
  73. end

令我比较欣慰的是,代码还算比较短,效率也算是不错吧(希望是真的)。说实话,Dijkstra算法在图论中真的很有效,但是提到游戏中的寻路,大家先想到的绝对是A星算法。这是因为游戏中的图有特殊的结构,而Dijkstra算法的一般性太强。不过呢,Dijkstra在RM地图上退化成了BFS算法这点让我很惊讶。

PS:我看了myownroc的BFS寻路,发现脚本的结构不太一样,我的循环层数似乎少一些,不知道原理是否是一致的。
PPS:至于这东西用在游戏里面合适与否,我也不知道,不过还是慎用吧。

Lv5.捕梦者 (版主)

遠航の猫咪

梦石
3
星屑
23191
在线时间
2387 小时
注册时间
2005-10-15
帖子
1166

开拓者

9
发表于 2014-12-20 17:36:52 | 只看该作者
理论上从起点和终点同时BFS(直到两者边界相交)会大大减少遍历的次数
就是把o(n^2)变成了o(2(n/2)^2),也就是1/2n^2,相当于原来的2倍效率

点评

如果找到“中点”,然后将寻路过程不断拆分为起点到中点、中点到终点这样两个过程(有点像分治法),时间复杂度就会变成O(nlogn)?  发表于 2014-12-20 19:42
SailCat (小猫子·要开心一点) 共上站 24 次,发表过 11 篇文章 上 次 在: [2006年01月28日11:41:18 星期六] 从 [162.105.120.91] 到本站一游。
回复 支持 反对

使用道具 举报

Lv1.梦旅人

梦石
0
星屑
50
在线时间
28 小时
注册时间
2014-11-7
帖子
14
8
发表于 2014-12-10 21:36:47 | 只看该作者
正方形方格图这种比较适合广度优先遍历
回复 支持 反对

使用道具 举报

Lv3.寻梦者

梦石
0
星屑
1759
在线时间
2524 小时
注册时间
2010-10-12
帖子
1454

开拓者

7
发表于 2014-12-8 02:23:23 | 只看该作者
我提个问题
A* 跟你们说的 Dijkstra 或者 BFS 哪个效率比较高
不知道若干(可能几十)事件一起运行的话RM会不会卡

点评

A*比较快些,Dijkstra在这里等同于BFS,一起弄的话没测试过,感觉会比较卡。RM的效率你懂的  发表于 2014-12-8 08:49

回复 支持 反对

使用道具 举报

Lv3.寻梦者

梦石
0
星屑
3583
在线时间
3065 小时
注册时间
2011-11-17
帖子
980
6
发表于 2014-11-29 23:25:49 | 只看该作者
为何用bfs 好写 随手就写来了 QA没觉得卡 玩家没觉得卡 那就这样了 在硬件越来越牛逼得现在 cpu的瓶颈越来越少了 算法地位也就低了 如果效率的提升肉眼看不出 我宁可用低效的 简单的 可维护性强的,如果真因为这个卡了 再改A*

点评

禾西的A*效率实在高=_=可以去看看  发表于 2014-11-30 03:20
说的没错,这个bfs已经是比较短了,我看A*的实现过程好长。确实在地图不是很大的时候bfs效果也不错  发表于 2014-11-29 23:50
算法导论里 和 Dijkstra一起写的 bellman算法 是支持负权制的 不过貌似实际运用中很少用到  发表于 2014-11-29 23:28

评分

参与人数 1星屑 +60 收起 理由
RyanBern + 60 塞糖

查看全部评分

回复 支持 反对

使用道具 举报

Lv5.捕梦者

梦石
0
星屑
33479
在线时间
5108 小时
注册时间
2012-11-19
帖子
4878

开拓者

5
发表于 2014-11-29 19:22:04 | 只看该作者
这种寻路就好像是透视性的,把整张地图”看“完后,找到通向目标的捷径。
那 RyanBern 有没有试过局部的”尝试性“的寻路呢?
比如:上方不能通行-->尝试左/右,如果能通过,往左/右一步,然后再转向最初的方向(上);如果都不能
通过 --> 后退N步,再往左或右一步(小绕),再回到最初方向(上)<<循环,直到上方能通过或设定的循环次数限制。

点评

跟默认接近的区别是,默认的只会接近但失败了不会找别的路,这个方法更像一个真人  发表于 2014-12-2 20:00
感觉有些像RM默认的“接近”移动方式呢……RTS游戏的寻路(例如星际,魔兽等)基本都是A星算法吧(这个不确定)  发表于 2014-11-29 20:56
B* 就是这个,好处就是你说的,嗯。只要墙不坑爹的话效率还是相当高的。  发表于 2014-11-29 20:16
↓没见过B*什么的,只是觉得这样比较适合于鼠标指挥角色移动;或是那些有”视野“性质的游戏。  发表于 2014-11-29 19:27
感觉像BFS  发表于 2014-11-29 19:26

评分

参与人数 1星屑 +60 收起 理由
RyanBern + 60 B*

查看全部评分

xp vx va mv  va mz 各类型脚本/插件定制
回复 支持 反对

使用道具 举报

Lv4.逐梦者

梦石
0
星屑
9280
在线时间
2504 小时
注册时间
2011-5-20
帖子
15389

开拓者

4
发表于 2014-11-29 17:46:40 | 只看该作者
用假位移呢?

点评

QwQ  发表于 2014-11-29 18:47
斜眼笑  发表于 2014-11-29 18:42
=_=好吧说的貌似不是一个东西···  发表于 2014-11-29 18:38
你知道楼主在说什么吗=_=  发表于 2014-11-29 18:30
回复 支持 反对

使用道具 举报

Lv1.梦旅人

梦石
0
星屑
60
在线时间
705 小时
注册时间
2007-12-23
帖子
874
3
发表于 2014-11-29 17:13:30 | 只看该作者
每一步都代价固定为1,所以一定就是BFS吧。。 真是麻烦了LZ给我们推导了这么半天///

点评

所以Dijkstra算法真心不适合出现在这种正方形网格地图上  发表于 2014-11-29 20:53
买了正版RMMV的同学进来看一下,谢谢~
https://rpg.blue/thread-393237-1-1.html
回复 支持 反对

使用道具 举报

Lv3.寻梦者

梦石
0
星屑
2749
在线时间
2630 小时
注册时间
2013-1-16
帖子
5657

贵宾

2
发表于 2014-11-27 20:29:05 | 只看该作者
本帖最后由 myownroc 于 2014-11-27 20:38 编辑

我的循环多一层的原因是我用了两个变量(ij)对地图的点进行遍历,如果改成一个自然就少了一层。而且我没有用队列来储存点,用的是二维数组,这样效率低了一点(回头弄个队列的来玩玩)。ps:我怎么觉得你的循环只有一层?

Dijkstra对图的权有所判断,而BFS没用到,所以这个Dijkstra像BFS。
另外当起点终点同在一集合就可以退出循环了吧?

点评

听说数组的实现是两头延长,shift也能O(1)吧  发表于 2014-11-28 20:39
就这样吧,我就不造队列模型了,其实用数组可以造队列模型,不过有些麻烦就是了  发表于 2014-11-27 21:04
这个是数组,删除第一个成员不会向队列/链表那样改一下头节点指针就完了,后面其他成员的下标也要改变,所以应该要有一个循环。  发表于 2014-11-27 20:58
不知道shift是如何实现的,正常来讲删除队列头部的复杂度是O(1),如果用的循环队列的话。其实这个东西优化效果比较明显  发表于 2014-11-27 20:48
我知道为什么我感觉你只有一层循环了:queue.shift隐含了一层循环,实际上你的循环也是两层。  发表于 2014-11-27 20:45

评分

参与人数 1星屑 +60 收起 理由
RyanBern + 60 塞糖

查看全部评分

(Created by @喵kano)


施工现场:hotege.github.io
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册会员

本版积分规则

拿上你的纸笔,建造一个属于你的梦想世界,加入吧。
 注册会员
找回密码

站长信箱:[email protected]|手机版|小黑屋|无图版|Project1游戏制作

GMT+8, 2024-11-25 14:06

Powered by Discuz! X3.1

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表