mirror of
https://github.com/cyberarm/i-mic-rts.git
synced 2025-12-15 15:52:34 +00:00
Roughed in some basic pathfinding, adapted from CitySim's
This commit is contained in:
@@ -28,6 +28,7 @@ require_relative "lib/zorder"
|
|||||||
require_relative "lib/entity"
|
require_relative "lib/entity"
|
||||||
require_relative "lib/map"
|
require_relative "lib/map"
|
||||||
require_relative "lib/tiled_map"
|
require_relative "lib/tiled_map"
|
||||||
|
require_relative "lib/pathfinder"
|
||||||
|
|
||||||
require_relative "lib/order"
|
require_relative "lib/order"
|
||||||
require_relative "lib/friendly_hash"
|
require_relative "lib/friendly_hash"
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ class IMICRTS
|
|||||||
@players.find { |player| player.id == id }
|
@players.find { |player| player.id == id }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def find_path(player:, entity:, goal:, travels_along: :ground, allow_diagonal: false, klass: IMICRTS::Pathfinder::BasePathfinder)
|
||||||
|
if klass.cached_path(entity, goal, travels_along)
|
||||||
|
puts "using a cached path!" if true#Setting.enabled?(:debug_mode)
|
||||||
|
return klass.cached_path(entity, goal, travels_along)
|
||||||
|
end
|
||||||
|
|
||||||
|
klass.new(director: self, entity: entity, goal: goal, travels_along: travels_along, allow_diagonal: allow_diagonal)
|
||||||
|
end
|
||||||
|
|
||||||
def record_order(order_id, *args)
|
def record_order(order_id, *args)
|
||||||
if order = Order.get(order_id)
|
if order = Order.get(order_id)
|
||||||
struct = order.struct(args)
|
struct = order.struct(args)
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ IMICRTS::Entity.define_entity(:harvester, :unit, 1400, "Harvests ore") do |entit
|
|||||||
end
|
end
|
||||||
|
|
||||||
entity.define_singleton_method(:seek_ore) do
|
entity.define_singleton_method(:seek_ore) do
|
||||||
ore = director.map.ores.compact.sort_by { |ore| next unless ore; ore.position.distance(entity.position) }.first
|
# ore = director.map.ores.compact.sort_by { |ore| next unless ore; ore.position.distance(entity.position) }.first
|
||||||
|
|
||||||
n = (ore.position - entity.position).normalized
|
# n = (ore.position - entity.position).normalized
|
||||||
n.z = 0
|
# n.z = 0
|
||||||
entity.position += n * 3
|
# entity.position += n * 3
|
||||||
end
|
end
|
||||||
|
|
||||||
entity.define_singleton_method(:seek_refinery) do
|
entity.define_singleton_method(:seek_refinery) do
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class IMICRTS
|
|||||||
@id = id
|
@id = id
|
||||||
@position = position
|
@position = position
|
||||||
@angle = angle
|
@angle = angle
|
||||||
|
@director = director
|
||||||
|
|
||||||
@radius = 32 / 2
|
@radius = 32 / 2
|
||||||
@target = nil
|
@target = nil
|
||||||
@@ -66,6 +67,7 @@ class IMICRTS
|
|||||||
|
|
||||||
def target=(entity)
|
def target=(entity)
|
||||||
@target = entity
|
@target = entity
|
||||||
|
@path = @director.find_path(player: @player, entity: self, goal: @target)
|
||||||
end
|
end
|
||||||
|
|
||||||
def hit?(x_or_vector, y = nil)
|
def hit?(x_or_vector, y = nil)
|
||||||
@@ -92,6 +94,9 @@ class IMICRTS
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
rotate_towards(@target) if @target && @movement
|
rotate_towards(@target) if @target && @movement
|
||||||
|
|
||||||
|
if @movement
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def tick(tick_id)
|
def tick(tick_id)
|
||||||
@@ -114,6 +119,15 @@ class IMICRTS
|
|||||||
def draw_gizmos
|
def draw_gizmos
|
||||||
Gosu.draw_rect(@position.x - @radius, @position.y - (@radius + 2), @radius * 2, 2, Gosu::Color::GREEN, ZOrder::ENTITY_GIZMOS)
|
Gosu.draw_rect(@position.x - @radius, @position.y - (@radius + 2), @radius * 2, 2, Gosu::Color::GREEN, ZOrder::ENTITY_GIZMOS)
|
||||||
|
|
||||||
|
if @path
|
||||||
|
@path.path.each_with_index do |node, i|
|
||||||
|
next_node = @path.path.dig(i + 1)
|
||||||
|
if next_node
|
||||||
|
Gosu.draw_line(node.tile.position.x, node.tile.position.y, Gosu::Color::RED, next_node.tile.position.x, next_node.tile.position.y, Gosu::Color::RED, ZOrder::ENTITY_GIZMOS)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if @target.is_a?(IMICRTS::Entity)
|
if @target.is_a?(IMICRTS::Entity)
|
||||||
Gosu.draw_line(@position.x, @position.y, @target_color, @target.position.x, @target.position.y, @target_color, ZOrder::ENTITY_GIZMOS) if @target
|
Gosu.draw_line(@position.x, @position.y, @target_color, @target.position.x, @target.position.y, @target_color, ZOrder::ENTITY_GIZMOS) if @target
|
||||||
else
|
else
|
||||||
|
|||||||
61
lib/map.rb
61
lib/map.rb
@@ -1,7 +1,5 @@
|
|||||||
class IMICRTS
|
class IMICRTS
|
||||||
class Map
|
class Map
|
||||||
Tile = Struct.new(:position, :color, :image, :state, :type)
|
|
||||||
|
|
||||||
attr_reader :tile_size, :tiles, :ores
|
attr_reader :tile_size, :tiles, :ores
|
||||||
def initialize(map_file:)
|
def initialize(map_file:)
|
||||||
@tiled_map = TiledMap.new(map_file)
|
@tiled_map = TiledMap.new(map_file)
|
||||||
@@ -9,8 +7,8 @@ class IMICRTS
|
|||||||
@width, @height = @tiled_map.width, @tiled_map.height
|
@width, @height = @tiled_map.width, @tiled_map.height
|
||||||
@tile_size = @tiled_map.tile_size
|
@tile_size = @tiled_map.tile_size
|
||||||
|
|
||||||
@tiles = []
|
@tiles = {}
|
||||||
@ores = []
|
@ores = {}
|
||||||
|
|
||||||
@tiled_map.layers.each do |layer|
|
@tiled_map.layers.each do |layer|
|
||||||
layer.height.times do |y|
|
layer.height.times do |y|
|
||||||
@@ -27,13 +25,16 @@ class IMICRTS
|
|||||||
|
|
||||||
def add_terrain(x, y, tile_id)
|
def add_terrain(x, y, tile_id)
|
||||||
if tile = @tiled_map.get_tile(tile_id - 1)
|
if tile = @tiled_map.get_tile(tile_id - 1)
|
||||||
@tiles << Tile.new(
|
_tile = Tile.new(
|
||||||
CyberarmEngine::Vector.new(x * @tile_size, y * @tile_size, ZOrder::TILE),
|
position: CyberarmEngine::Vector.new(x * @tile_size, y * @tile_size, ZOrder::TILE),
|
||||||
nil,
|
image: tile.image,
|
||||||
tile.image,
|
visible: true,
|
||||||
:yes,
|
type: tile.data.type.to_sym,
|
||||||
tile.data.type
|
tile_size: @tile_size
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@tiles[x] ||= {}
|
||||||
|
@tiles[x][y] = _tile
|
||||||
else
|
else
|
||||||
raise "No such tile!"
|
raise "No such tile!"
|
||||||
end
|
end
|
||||||
@@ -41,15 +42,16 @@ class IMICRTS
|
|||||||
|
|
||||||
def add_ore(x, y, tile_id)
|
def add_ore(x, y, tile_id)
|
||||||
if tile = @tiled_map.get_tile(tile_id - 1)
|
if tile = @tiled_map.get_tile(tile_id - 1)
|
||||||
@ores << Tile.new(
|
_ore = Tile.new(
|
||||||
CyberarmEngine::Vector.new(x * @tile_size, y * @tile_size, ZOrder::ORE),
|
position: CyberarmEngine::Vector.new(x * @tile_size, y * @tile_size, ZOrder::ORE),
|
||||||
nil,
|
image: tile.image,
|
||||||
tile.image,
|
visible: true,
|
||||||
:yes,
|
type: nil,
|
||||||
nil
|
tile_size: @tile_size
|
||||||
)
|
)
|
||||||
else
|
|
||||||
@ores << nil
|
@ores[x] ||= {}
|
||||||
|
@ores[x][y] = _ore
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -80,11 +82,11 @@ class IMICRTS
|
|||||||
next if _y < 0 || _y > @height
|
next if _y < 0 || _y > @height
|
||||||
|
|
||||||
if tile = tile_at(_x, _y)
|
if tile = tile_at(_x, _y)
|
||||||
_tiles.push(tile) if tile.state != :unexplored
|
_tiles.push(tile) if tile.visible
|
||||||
end
|
end
|
||||||
|
|
||||||
if ore = ore_at(_x, _y)
|
if ore = ore_at(_x, _y)
|
||||||
_tiles.push(ore) if ore.state != :unexplored
|
_tiles.push(ore) if ore.visible
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -93,11 +95,26 @@ class IMICRTS
|
|||||||
end
|
end
|
||||||
|
|
||||||
def tile_at(x, y)
|
def tile_at(x, y)
|
||||||
@tiles[x + y * @width]
|
@tiles.dig(x, y)
|
||||||
end
|
end
|
||||||
|
|
||||||
def ore_at(x, y)
|
def ore_at(x, y)
|
||||||
@ores[x + y * @width]
|
@ores.dig(x, y)
|
||||||
|
end
|
||||||
|
|
||||||
|
class Tile
|
||||||
|
attr_accessor :position, :grid_position, :image, :visible, :entity, :type
|
||||||
|
def initialize(position:, image:, visible:, type:, tile_size:)
|
||||||
|
@position = position
|
||||||
|
@grid_position = position.clone
|
||||||
|
@grid_position /= tile_size
|
||||||
|
@grid_position.x, @grid_position.y = @grid_position.x.floor, @grid_position.y.floor
|
||||||
|
|
||||||
|
@image = image
|
||||||
|
@visible = visible
|
||||||
|
@entity = nil
|
||||||
|
@type = type
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
8
lib/pathfinder.rb
Normal file
8
lib/pathfinder.rb
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
class IMICRTS
|
||||||
|
class Pathfinder
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Dir.glob("#{IMICRTS::GAME_ROOT_PATH}/lib/pathfinding/*.rb").each do |pathing_method|
|
||||||
|
require_relative pathing_method
|
||||||
|
end
|
||||||
189
lib/pathfinding/base_pathfinder.rb
Normal file
189
lib/pathfinding/base_pathfinder.rb
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
class IMICRTS
|
||||||
|
class Pathfinder
|
||||||
|
class BasePathfinder
|
||||||
|
Node = Struct.new(:tile, :parent, :distance, :cost)
|
||||||
|
CACHE = {}
|
||||||
|
|
||||||
|
def self.cached_path(source, goal, travels_along)
|
||||||
|
found_path = CACHE.dig(travels_along, source, goal)
|
||||||
|
if found_path
|
||||||
|
found_path = nil unless found_path.valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
return found_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.cache_path(path)
|
||||||
|
CACHE[path.travels_along] ||= {}
|
||||||
|
CACHE[path.travels_along][path.source] ||= {}
|
||||||
|
CACHE[path.travels_along][path.source][path.goal] = path
|
||||||
|
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :map, :source, :goal, :travels_along, :allow_diagonal
|
||||||
|
attr_reader :path, :age
|
||||||
|
def initialize(director:, entity:, goal:, travels_along: :ground, allow_diagonal: false)
|
||||||
|
@director = director
|
||||||
|
@map = @director.map
|
||||||
|
@entity = entity
|
||||||
|
_goal = goal.clone
|
||||||
|
_goal /= @map.tile_size
|
||||||
|
_goal.x, _goal.y = _goal.x.floor, _goal.y.floor
|
||||||
|
@goal = _goal
|
||||||
|
|
||||||
|
@travels_along = travels_along
|
||||||
|
@allow_diagonal = allow_diagonal
|
||||||
|
@age = Gosu.milliseconds
|
||||||
|
|
||||||
|
@created_nodes = 0
|
||||||
|
@nodes = []
|
||||||
|
@path = []
|
||||||
|
@tiles = @map.tiles.values.map { |columns| columns.values }.flatten.select { |tile| tile }
|
||||||
|
|
||||||
|
@visited = Hash.new do |hash, value|
|
||||||
|
hash[value] = Hash.new {|h, v| h[v] = false}
|
||||||
|
end
|
||||||
|
|
||||||
|
@depth = 0
|
||||||
|
@max_depth = @tiles.size
|
||||||
|
@seeking = true
|
||||||
|
|
||||||
|
position = entity.position.clone
|
||||||
|
position.x, position.y = position.x.floor, position.y.floor
|
||||||
|
position /= @map.tile_size
|
||||||
|
|
||||||
|
@current_node = create_node(position.x, position.y)
|
||||||
|
unless @current_node
|
||||||
|
puts "Failed to find path!" if true
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@current_node.distance = 0
|
||||||
|
@current_node.cost = 0
|
||||||
|
add_node @current_node
|
||||||
|
|
||||||
|
find
|
||||||
|
|
||||||
|
Pathfinder.cache_path(self) if @path.size > 0 && false#Setting.enabled?(:cache_paths)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks if Map still has all of paths required tiles
|
||||||
|
def valid?
|
||||||
|
valid = true
|
||||||
|
@path.each do |node|
|
||||||
|
unless @map.tiles.dig(node.tile.grid_position.x, node.tile.grid_position.y).entity == node.tile.entity
|
||||||
|
valid = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return valid
|
||||||
|
end
|
||||||
|
|
||||||
|
def find
|
||||||
|
while(@seeking && @depth < @max_depth)
|
||||||
|
seek
|
||||||
|
end
|
||||||
|
|
||||||
|
if @depth >= @max_depth
|
||||||
|
puts "Failed to find path from: #{@source.x}:#{@source.y} (#{@map.grid.dig(@source.x,@source.y).element.class}) to: #{@goal.position.x}:#{@goal.position.y} (#{@goal.element.class}) [#{@depth}/#{@max_depth} depth]" if true#Setting.enabled?(:debug_mode)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def at_goal?
|
||||||
|
@current_node.tile.grid_position.distance(@goal) < 1.1
|
||||||
|
end
|
||||||
|
|
||||||
|
def seek
|
||||||
|
unless @current_node && @map.tiles.dig(@goal.x, @goal.y)
|
||||||
|
@seeking = false
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@nodes.delete(@current_node) # delete visited nodes
|
||||||
|
@visited[@current_node.tile.grid_position.x][@current_node.tile.grid_position.y] = true
|
||||||
|
|
||||||
|
if at_goal?
|
||||||
|
until(@current_node.parent.nil?)
|
||||||
|
@path << @current_node
|
||||||
|
@current_node = @current_node.parent
|
||||||
|
end
|
||||||
|
@path.reverse!
|
||||||
|
|
||||||
|
@seeking = false
|
||||||
|
puts "Generated path with #{@path.size} steps, #{@created_nodes} nodes created. [#{@depth}/#{@max_depth} depth]" if true#Setting.enabled?(:debug_mode)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
#LEFT
|
||||||
|
add_node create_node(@current_node.tile.grid_position.x - 1, @current_node.tile.grid_position.y, @current_node)
|
||||||
|
# RIGHT
|
||||||
|
add_node create_node(@current_node.tile.grid_position.x + 1, @current_node.tile.grid_position.y, @current_node)
|
||||||
|
# UP
|
||||||
|
add_node create_node(@current_node.tile.grid_position.x, @current_node.tile.grid_position.y - 1, @current_node)
|
||||||
|
# DOWN
|
||||||
|
add_node create_node(@current_node.tile.grid_position.x, @current_node.tile.grid_position.y + 1, @current_node)
|
||||||
|
|
||||||
|
# TODO: Add diagonal nodes, if requested
|
||||||
|
if @allow_diagonal
|
||||||
|
end
|
||||||
|
|
||||||
|
@current_node = next_node
|
||||||
|
@depth += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def node_visited?(node)
|
||||||
|
@visited[node.tile.grid_position.x][node.tile.grid_position.y]
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_node(node)
|
||||||
|
return unless node
|
||||||
|
|
||||||
|
@nodes << node
|
||||||
|
return node
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_node(x, y, parent = nil)
|
||||||
|
return unless tile = @map.tiles.dig(x, y)
|
||||||
|
return unless tile.type == @travels_along
|
||||||
|
return if tile.entity
|
||||||
|
return if @visited.dig(x, y)
|
||||||
|
return if @nodes.detect {|node| node.tile.grid_position.x == x && node.tile.grid_position.y == y}
|
||||||
|
|
||||||
|
node = Node.new
|
||||||
|
node.tile = tile
|
||||||
|
node.parent = parent
|
||||||
|
node.distance = parent.distance + 1 if parent
|
||||||
|
node.cost = 0
|
||||||
|
|
||||||
|
@created_nodes += 1
|
||||||
|
return node
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_node
|
||||||
|
fittest = nil
|
||||||
|
fittest_distance = Float::INFINITY
|
||||||
|
|
||||||
|
distance = nil
|
||||||
|
@nodes.each do |node|
|
||||||
|
next if node == @current_node
|
||||||
|
|
||||||
|
distance = node.tile.grid_position.distance(@goal)
|
||||||
|
|
||||||
|
if distance < fittest_distance
|
||||||
|
if fittest && (node.distance + node.cost) < (fittest.distance + fittest.cost)
|
||||||
|
fittest = node
|
||||||
|
fittest_distance = distance
|
||||||
|
else
|
||||||
|
fittest = node
|
||||||
|
fittest_distance = distance
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return fittest
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -116,7 +116,7 @@ class IMICRTS
|
|||||||
World Mouse Y: #{mouse.y}
|
World Mouse Y: #{mouse.y}
|
||||||
|
|
||||||
Director Tick: #{@director.current_tick}
|
Director Tick: #{@director.current_tick}
|
||||||
Tile: X: #{tile.position.x} Y: #{tile.position.y} Type: #{tile.type}
|
#{ tile ? "Tile: X: #{tile.position.x} Y: #{tile.position.y} Type: #{tile.type}" : ""}
|
||||||
).lines.map { |line| line.strip }.join("\n")
|
).lines.map { |line| line.strip }.join("\n")
|
||||||
|
|
||||||
@debug_info.x = @sidebar.width + 20
|
@debug_info.x = @sidebar.width + 20
|
||||||
|
|||||||
Reference in New Issue
Block a user