查看: 3086|回复: 3

[RMVA发布] TyPath - VA智能寻路脚本

[复制链接]
梦石
2
星屑
6767
在线时间
501 小时
注册时间
2018-3-23
回帖
487

R考场第七期银奖

发表于 2020-3-14 18:16:21 | 显示全部楼层 |阅读模式

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

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

×
本帖最后由 MCCF 于 2020-9-25 23:04 编辑

原贴位于RPG Maker VX Ace 讨论区

众所周知,RPG Maker原版的寻路是不完备的,时常会遇到被墙壁/桌子卡住的现象。
TyPath脚本用A*算法扩展了原有寻路,部分解决了这个问题,并为更多的扩展和支持提供基础。

[fold=关于TyPath]
TyPath是基于A*算法实现的。GameDev的A*页面 及 Amit的A*页面 让我得到了很大的启示。
另外,具体在RGSS下的实现参考了 @余烬之中 的 A*实现指南。在此对他们表示诚挚的谢意。
[/fold]
[fold=前言]
有时,我们需要让事件自主移动向角色。RPG Maker默认的 事件-自主移动-接近 解决了这一问题。
然而,顾名思义,这仅仅是朝相对离角色更进的方向移动一步。
如果在这个方向上有一障碍(尤其是凹形的那种),就有几率会被卡在障碍处。
在追逐战中,敌人被卡住的现象很影响玩家体验。
TyPath就是用于解决这个问题,使得即使在一个迷宫中,事件也能正确寻路到玩家处。
[/fold]
[fold=脚本]
[pre lang="ruby"]#==============================================================================
# TyPath寻路 V1.2
#    by MCCF
#------------------------------------------------------------------------------
# [说明]
#    - RPG Maker自带的寻路基于“接近”或“远离”的概念,因此过程中会不断接近目标;
#      但是,如果中途有一不可通行的障碍,则即使有另外的可行路径也不会去寻找。
#    - 本脚本通过扩展的方式,使用A*算法提高寻路的精确度和灵活度,并且可以保证在有
#      路径时找到可通行的路径。
#    - 有一种“堵截式寻路”的设定,该设定指示在抵达玩家附近后就会停止不动,而不是
#      像默认脚本一样随机移开。这可能使得玩家被敌人堵住去路。
#
#    - 如果存在BUG或兼容性问题,如反馈我会尽量修复。该脚本也是基础脚本,可以用于其
#      它大型系统的制作。
#------------------------------------------------------------------------------
# [使用方法]
#    - 复制到Main之前
#
#    - 在事件页的某个注释中添加<typath>,则该事件-自主移动中使用的“接近”选项会由
#      TyPath进行处理。
#    - 在事件页的某个注释中添加<typath_blocked>,则该事件-自主移动中使用的“接近”
#      选项会使用堵截式寻路。
#    - 在事件页的某个注释中添加<typath_limit = value>,则该事件-自主移动中使用的
#      “接近”选项会使用value作为寻路精度上限(详见配置模块)。
#
#    - 在移动路线中使用脚本typath_move_toward(id),可以移动到第ID号人物处。ID为正
#      时表示地图事件ID,为0表示本事件,为负数表示玩家队伍(如-2为队伍第二个角色)。
#    - 面向脚本人员:Game_Character#move_typath(x,y)用于强制角色移动到指定坐标,
#      Game_Character#move_typath_to(character)用于强制角色移动到指定人物,
#------------------------------------------------------------------------------
# [使用条款]
#    - 只要署名MCCF即可。只要不以自己的名义发布,您可以自由修改此脚本。
#      鼓励将此脚本在署名的情况下用作其它脚本的基础脚本。
#------------------------------------------------------------------------------
# [兼容性]
#    - 此脚本默认为四方向寻路,不会试图斜向移动,但是应该与八方向移动脚本没有冲突。
#    - 重写了关于事件寻路的部分,对于像素级移动等改动较大的脚本可能不兼容。
#------------------------------------------------------------------------------
#  通用配置模块  
#==============================================================================
module TyPath_Config
  
  # 基本设置 ------------------------------------------------------------------
  
  # 是否默认用TyPath替换原生寻路
  DEFAULT_TYPATH = false
  
  # 是否默认启用堵截式寻路
  TYPATH_BLOCKED = true
  
  # 控制是否用TyPath替换默认寻路的开关ID
  # 为0则不启用开关控制
  TYPATH_SWITCH_ID = 0
  
  # 性能设置 ------------------------------------------------------------------
  
  # 寻路随机性(0-9,数值越大随机性越高)
  # 增加随机性可进一步模拟原生寻路,减少玩家难度。
  TYPATH_RANDOM_RATE = 0
  
  # 寻路智能度(0-9,数值越小越智能)
  # 如果严重卡顿请适当增加
  TYPATH_SEARCH_RATE = 0
  
  # 默认寻路精度(为0则无限制)
  # 如果严重卡顿请适当减小
  TYPATH_SEARCH_LIMIT = 0
  
  # 附加设置 ------------------------------------------------------------------
  
  # 有害地形的代价修正
  # 数值越大则TyPath越会避免通过有害地形
  DAMAGE_FLOOR_CORRECT = 2
  
end
#------------------------------------------------------------------------------
#  配置模块结束  
#==============================================================================

#==============================================================================
# ■ TyPath_Core
#------------------------------------------------------------------------------
#   TyPath核心寻路模块。
#==============================================================================

module TyPath_Core
  #--------------------------------------------------------------------------
  # ● 获取方向的数组
  #--------------------------------------------------------------------------
  def self.directions
    [2, 4, 6, 8]
  end
  #--------------------------------------------------------------------------
  # ● 节点类
  #--------------------------------------------------------------------------
  class Point
    #--------------------------------------------------------------------------
    # ● 定义实例变量
    #--------------------------------------------------------------------------
    attr_reader :x, :y                    # X坐标及Y坐标
    attr_reader :g_val, :h_val            # G值及H值
    attr_reader :parent, :dir             # 当前父节点及抵达方向
    attr_reader :death                    # 深度
    #--------------------------------------------------------------------------
    # ● 初始化
    #--------------------------------------------------------------------------
    def initialize(cx, cy, tar_x, tar_y)
      @x = cx
      @y = cy
      @g_val = 0
      @h_val = heuristic(tar_x, tar_y)
      @parent = nil
      @dir = 0
      @death = 0
    end
    #--------------------------------------------------------------------------
    # ● 设置
    #--------------------------------------------------------------------------
    def setup(parent, dir)
      @parent = parent
      @dir = dir
      @death = parent.death + 1
      @g_val = parent.g_val + get_cost
    end
    #--------------------------------------------------------------------------
    # ● 启发式函数
    #--------------------------------------------------------------------------
    def heuristic(tar_x, tar_y)
      ((tar_x - @x).abs + (tar_y - @y).abs) * 10
    end
    #--------------------------------------------------------------------------
    # ● 获取抵达成本
    #--------------------------------------------------------------------------
    def get_cost
      return 10 * TyPath_Config::DAMAGE_FLOOR_CORRECT if $game_map.damage_floor?(@x, @y)
      return 10
    end
  end
  #--------------------------------------------------------------------------
  # ● 节点列表类
  #--------------------------------------------------------------------------
  class PointList
    #--------------------------------------------------------------------------
    # ● 定义实例变量
    #--------------------------------------------------------------------------
    attr_accessor :list                   # 节点数组
    #--------------------------------------------------------------------------
    # ● 初始化
    #--------------------------------------------------------------------------
    def initialize(*origin)
      @list = [*origin]
    end
    #--------------------------------------------------------------------------
    # ● 判定是否为空
    #--------------------------------------------------------------------------
    def empty?
      @list.empty?
    end
    #--------------------------------------------------------------------------
    # ● 取出最佳节点
    #--------------------------------------------------------------------------
    def shift
      @list.shift
    end
    #--------------------------------------------------------------------------
    # ● 加入节点
    #  使用二分查找优化效率。
    #--------------------------------------------------------------------------
    def insert(node)
      left = 0
      right = @list.length
      while left + 1 < right
        mid = (left + right) / 2
        if node.g_val + node.h_val > @list[mid].g_val + @list[mid].h_val
          left = mid
        else
          right = mid
        end
      end
      @list.insert(right, node)
    end
    #--------------------------------------------------------------------------
    # ● 删除节点
    #--------------------------------------------------------------------------
    def delete(node)
      @list.delete(node)
    end
    #--------------------------------------------------------------------------
    # ● 获取指定位置的节点
    #--------------------------------------------------------------------------
    def get_node(cur_x, cur_y)
      @list.find {|item| item.x == cur_x && item.y == cur_y }
    end
  end
  #--------------------------------------------------------------------------
  # ● 通行检查
  #--------------------------------------------------------------------------
  def self.passable?(char, cur_x, cur_y)
    directions.any? do |dir|
      x = $game_map.round_x_with_direction(cur_x, dir)
      y = $game_map.round_y_with_direction(cur_y, dir)
      char.passable?(x, y, 10 - dir)
    end
  end
  #--------------------------------------------------------------------------
  # ● 地图通行检查
  #--------------------------------------------------------------------------
  def self.map_passable?(char, cur_x, cur_y)
    directions.any? do |dir|
      x = $game_map.round_x_with_direction(cur_x, dir)
      y = $game_map.round_y_with_direction(cur_y, dir)
      $game_map.valid?(x, y) && $game_map.passable?(x, y, 10 - dir) &&
      $game_map.events_xy_nt(x, y).all? do |event|
        !event.normal_priority? ||
        (char.is_a?(Game_Event) && char.id == event.id)
      end
    end
  end
  #--------------------------------------------------------------------------
  # ● 检查两点是否邻近
  #--------------------------------------------------------------------------
  def self.near?(x, y, cur_x, cur_y)
    distance_x(x, cur_x) + distance_y(y, cur_y) == 0
  end
  #--------------------------------------------------------------------------
  # ● 生成接近指定角色的TyPath路径
  #--------------------------------------------------------------------------
  def self.create_route_to(char, tar_char, strict = true, limit = TyPath_Config::TYPATH_SEARCH_LIMIT)
    tar_x = tar_char.x
    tar_y = tar_char.y
    return [] if char.x == tar_x && char.y == tar_y
    return [] unless map_passable?(char, tar_x, tar_y)
    origin = Point.new(char.x, char.y, tar_x, tar_x)
    open  = PointList.new(origin)
    close = PointList.new()
    until open.empty?
      node = open.shift
      directions.each do |dir|
        next unless limit == 0 || node.death < limit
        x = $game_map.round_x_with_direction(node.x, dir)
        y = $game_map.round_y_with_direction(node.y, dir)
        if x == tar_x && y == tar_y
          child = Point.new(x, y, tar_x, tar_y)
          child.setup(node, dir)
          open.insert(child)
        else
          next unless char.passable?(node.x, node.y, dir)
          if temp = open.get_node(x, y)
            if node.g_val + temp.get_cost < temp.g_val
              temp.setup(node, dir)
            end
          elsif temp = close.get_node(x, y)
            if false
              temp.setup(node, dir)
              close.delete(temp)
              open.insert(temp)
            end
          else
            child = Point.new(x, y, tar_x, tar_y)
            child.setup(node, dir)
            open.insert(child)
          end
        end
      end
      close.insert(node)
      break if close.get_node(tar_x, tar_y)
    end
    if strict
      if cur = close.get_node(tar_x, tar_y)
        routes = []
        while cur
          routes.unshift(cur.dir)
          cur = cur.parent
        end
        return routes
      else
        return []
      end
    else
      cur = close.list.min_by{|item| item.h_val }
      routes = []
      while cur
        routes.unshift(cur.dir)
        cur = cur.parent
      end
      return routes
    end
  end
  #--------------------------------------------------------------------------
  # ● 生成TyPath路径
  #  strict : 启用严格模式的标识。严格模式下如无法抵达目标,将会返回空数组。
  #--------------------------------------------------------------------------
  def self.create_route(char, tar_x, tar_y, strict = true, limit = TyPath_Config::TYPATH_SEARCH_LIMIT)
    return [] if char.x == tar_x && char.y == tar_y
    return [] unless passable?(char, tar_x, tar_y)
    origin = Point.new(char.x, char.y, tar_x, tar_x)
    open  = PointList.new(origin)
    close = PointList.new()
    until open.empty?
      node = open.shift
      directions.each do |dir|
        next unless limit == 0 || node.death < limit
        next unless char.passable?(node.x, node.y, dir)
        x = $game_map.round_x_with_direction(node.x, dir)
        y = $game_map.round_y_with_direction(node.y, dir)
          if temp = open.get_node(x, y)
            if node.g_val + temp.get_cost < temp.g_val
              temp.setup(node, dir)
            end
          elsif temp = close.get_node(x, y)
          if false
            temp.setup(node, dir)
            close.delete(temp)
            open.insert(temp)
          end
        else
          child = Point.new(x, y, tar_x, tar_y)
          child.setup(node, dir)
          open.insert(child)
        end
      end
      close.insert(node)
      break if close.get_node(tar_x, tar_y)
    end
    if strict
      if cur = close.get_node(tar_x, tar_y)
        routes = []
        while cur
          routes.unshift(cur)
          cur = cur.parent
        end
        return routes
      else
        return []
      end
    else
      cur = close.list.min_by{|item| item.h_val }
      routes = []
      while cur
        routes.unshift(cur)
        cur = cur.parent
      end
      return routes
    end
  end
  #--------------------------------------------------------------------------
  # ● 将TyPath路径转为标准移动指令
  #--------------------------------------------------------------------------
  def self.make_move_command(path)
    path.delete_if {|dir| dir.zero? }
    route = RPG::MoveRoute.new.tap do |route|
      route.repeat = false
      path.each do |dir|
        route.list.push(RPG::MoveCommand.new.tap{|cmd|cmd.code = dir >> 1})
      end
    end
  end
  #--------------------------------------------------------------------------
  # ● 检查注释是否匹配
  #--------------------------------------------------------------------------
  def self.match_comments(cmd, reg)
    return nil unless cmd.code == 108 || cmd.code == 408
    return cmd.parameters[0] =~ reg
  end
end

#==============================================================================
# ■ Game_Character
#------------------------------------------------------------------------------
#   添加了路径移动的地图人物类。
#==============================================================================

class Game_Character < Game_CharacterBase
  #--------------------------------------------------------------------------
  # ● 获取指定ID的人物
  #--------------------------------------------------------------------------
  def get_character(id)
    if $game_party.in_battle
      nil
    elsif param < 0
      $game_player
    else
      $game_map.events[param > 0 ? param : @event.id]
    end
  end
  #--------------------------------------------------------------------------
  # ● 根据TyPath寻路向指定ID的人物
  #--------------------------------------------------------------------------
  def typath_move_toward(id)
    character = get_character(id)
    move_typath_to(character) if character
  end
  #--------------------------------------------------------------------------
  # ● 根据TyPath寻路向指定坐标
  #--------------------------------------------------------------------------
  def move_typath(tar_x, tar_y)
    routes = TyPath_Core.create_route(self, tar_x, tar_y)
    routes.collect! {|item| item.dir }
    force_move_route(TyPath_Core.make_move_command(routes))
  end
  #--------------------------------------------------------------------------
  # ● 根据TyPath寻路向指定人物
  #--------------------------------------------------------------------------
  def move_typath_to(char)
    routes = TyPath_Core.create_route_to(self, char)
    force_move_route(TyPath_Core.make_move_command(routes))
  end
end

#==============================================================================
# ■ Game_Event
#------------------------------------------------------------------------------
#  处理事件的类。拥有条件判断、事件页的切换、并行处理、执行事件等功能。
#   在 Game_Map 类的内部使用。
#==============================================================================

class Game_Event < Game_Character
  #--------------------------------------------------------------------------
  # ● 自动移动的更新
  #--------------------------------------------------------------------------
  alias typath_update_self_movement update_self_movement
  def update_self_movement
    return if @move_type == 0
    if typath?
      if @stop_count > stop_count_threshold
        if @move_type == 2
          move_type_toward_player
        elsif near_the_screen?
          case @move_type
            when 1;  move_type_random
            when 2;  move_type_toward_player
            when 3;  move_type_custom
          end
        end
      end
    else
      typath_update_self_movement
    end
  end
  #--------------------------------------------------------------------------
  # ● 移动类型 : 接近(随机性处理)
  #--------------------------------------------------------------------------
  alias typath_origin_move_type_toward_player move_type_toward_player
  def typath_move_type_toward_player
    if rand(10) >= TyPath_Config::TYPATH_RANDOM_RATE
      move_toward_player
    else
      typath_origin_move_type_toward_player
    end
  end
  #--------------------------------------------------------------------------
  # ● 移动类型 : 接近
  #--------------------------------------------------------------------------
  def move_type_toward_player
    if typath?
      if update_typath
        if (dir = @typath_route.shift).nil?
          typath_move_type_toward_player unless typath_blocked?
        else
          move_straight(dir)
        end
      else
        typath_move_type_toward_player
      end
    else
      @typath_route = []
      typath_move_type_toward_player
    end
  end
  #--------------------------------------------------------------------------
  # ● 更新TyPath数据
  #--------------------------------------------------------------------------
  def update_typath
    if @typath_player_pos != [$game_player.x, $game_player.y]
      @typath_player_pos = [$game_player.x, $game_player.y]
      return false unless update_typath_route
    end
    return false unless update_typath_route unless @typath_route
    unless @typath_route[0] && passable?(@x, @y, @typath_route[0])
      return false unless update_typath_route
    end
    return true
  end
  #--------------------------------------------------------------------------
  # ● 更新TyPath路径
  #--------------------------------------------------------------------------
  def update_typath_route
    if rand(10) >= TyPath_Config::TYPATH_SEARCH_RATE
      @typath_route = TyPath_Core.create_route_to(self, $game_player, false, typath_limit)
      return true
    else
      return false
    end
  end
  #--------------------------------------------------------------------------
  # ● 检查TyPath是否启用
  #--------------------------------------------------------------------------
  def typath?
    result = @list.any? do |item|
      TyPath_Core::match_comments(item, /<typath>/i)
    end
    if result
      return true
    else
      if TyPath_Config::DEFAULT_TYPATH
        return true
      elsif TyPath_Config::TYPATH_SWITCH_ID != 0
        $game_switches[TyPath_Config::TYPATH_SWITCH_ID]
      end
    end
    return false
  end
  #--------------------------------------------------------------------------
  # ● 获取TyPath寻路精度
  #--------------------------------------------------------------------------
  def typath_limit
    @list.each do |item|
      if TyPath_Core::match_comments(item, /<typath_limit\s*=\s*(\d+)>/i)
        return $1.to_i
      end
    end
    return TyPath_Config::TYPATH_SEARCH_LIMIT
  end
  #--------------------------------------------------------------------------
  # ● 获取TyPath是否启用堵截式寻路
  #--------------------------------------------------------------------------
  def typath_blocked?
    result = @list.any? do |item|
      TyPath_Core::match_comments(item, /<typath_blocked>/i)
    end
    return true if result
    return TyPath_Config::TYPATH_BLOCKED
  end
end
[/pre]
[/fold]
[fold=范例工程]
请仔细关注范例中事件的内容,将玩家和事件移动到不同的岛屿上查看效果。
范例中不一定有最新脚本,如遇BUG请尝试本帖中的脚本,如仍发生问题请反馈。
(其实已经有已知BUG了……所以去帖子上面复制吧233)
1.0版本范例工程:
1.1版本范例工程:
1.2版本改动较小,不提供范例工程,请自行从帖子中复制最新脚本。
[/fold]
[fold=实现说明]
真的有人会看这个吗?

关于“堵截式寻路”:
“堵截式寻路”就是在路径为空时,不会继续采用原生方式寻路。这也意味着事件将对玩家穷追不舍,在已经抵达玩家附近时就会停止移动,可以做到堵截玩家的效果。

关于“性能设置”:
其中的“随机性”、“智能度”和“精度”是不是看上去都差不多?
实际上,这只是由于作者词穷而已……
出于提高性能的原因,有时候TyPath在路径还未计算出来时,不会进行昂贵的路径计算,而是会使用原版寻路(typath_move_type_towrad_player)。这样仅使得路径增长一两步,却能是性能提高到原来的数倍。
而“智能度”就是用于控制进行路径计算而非使用原版寻路的几率。这意味着,当智能度设置得较低(数值较大),可以使性能有较大的提升,但是事件也越有可能出现短时间卡在墙角的现象。
至于“精度”,则是模仿了RMMV自带的A*寻路的优化方式,通过把深度大于某个值的节点抛弃掉来使得搜索强度固定在一个值。这意味着,假如精度为N,而目标在N步都到不了的地方,则寻路仍然会采用不完备的启发式方式。精度设置是最直接有效的优化方式,但是对于寻路结果的影响很大,通常在地图较小不需要大量处理的情况下使用。
另外这个“随机性”则与性能优化关联不大,而只是影响在使用原版寻路时,是否进行随机移动的操作。如果仔细观察原版寻路,会发现事件有1/3的几率会随机移动,1/6的几率会直接朝当前方向前进一步。这也是为了提高寻路的随机性和游戏的平衡性。但是有时候,事件随机乱转令人很不愉快,因此可以将随机性调低或使用堵截式寻路,达到更好的效果。
[/fold]
[fold=更新日志]
1.1版本更新:
  • 现在即使目标不可到达,也会自动寻路到离目标最近的位置上。
  • 进一步优化寻路速度,减少寻路对游戏FPS的影响。
  • 可设置“堵截式寻路”,此时事件将忠贞不渝死缠烂打地堵截角色。
  • 可设置寻路精度及智能度,可以通过减小这两个设定值防止卡顿。
  • 可设置寻路避免通过有害地形的修正。
  • 以及更多奇奇怪怪的部分优化和一大堆BUG修复。

[/fold]
祝好。
头像被屏蔽
梦石
0
星屑
17103
在线时间
32 小时
注册时间
2020-3-16
回帖
29
发表于 2020-3-16 17:11:10 | 显示全部楼层
提示: 作者被禁止或删除 内容自动屏蔽
回复

使用道具 举报

梦石
0
星屑
166
在线时间
17 小时
注册时间
2022-2-2
回帖
9
发表于 2022-2-3 11:41:29 | 显示全部楼层
这个适合mz用吗,新手求教点哪里下载

点评

不适合的,va和mz语言都不一样  发表于 2022-2-3 12:40
回复

使用道具 举报

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

本版积分规则

Powered by Discuz! X5.0 © 2001-2026 Discuz! Team.

在本版发帖返回顶部