Update 2D navigation demos

This commit is contained in:
Aaron Franke
2020-02-03 02:36:35 -05:00
parent 7af4d281b9
commit b0e1cc0227
5 changed files with 174 additions and 179 deletions

View File

@@ -1,46 +1,44 @@
extends Navigation2D
export(float) var CHARACTER_SPEED = 400.0
export(float) var character_speed = 400.0
var path = []
func _process(delta):
var walk_distance = character_speed * delta
move_along_path(walk_distance)
# The 'click' event is a custom input action defined in
# Project > Project Settings > Input Map tab
# Project > Project Settings > Input Map tab.
func _input(event):
if not event.is_action_pressed('click'):
if not event.is_action_pressed("click"):
return
_update_navigation_path($Character.position, get_local_mouse_position())
func _update_navigation_path(start_position, end_position):
# get_simple_path is part of the Navigation2D class
# it returns a PoolVector2Array of points that lead you from the
# start_position to the end_position
path = get_simple_path(start_position, end_position, true)
# The first point is always the start_position
# We don't need it in this example as it corresponds to the character's position
path.remove(0)
set_process(true)
func _process(delta):
var walk_distance = CHARACTER_SPEED * delta
move_along_path(walk_distance)
func move_along_path(distance):
var last_point = $Character.position
while path.size():
var distance_between_points = last_point.distance_to(path[0])
# the position to move to falls between two points
# The position to move to falls between two points.
if distance <= distance_between_points:
$Character.position = last_point.linear_interpolate(path[0], distance / distance_between_points)
return
# the position is past the end of the segment
# The position is past the end of the segment.
distance -= distance_between_points
last_point = path[0]
path.remove(0)
# the character reached the end of the path
# The character reached the end of the path.
$Character.position = last_point
set_process(false)
func _update_navigation_path(start_position, end_position):
# get_simple_path is part of the Navigation2D class.
# It returns a PoolVector2Array of points that lead you
# from the start_position to the end_position.
path = get_simple_path(start_position, end_position, true)
# The first point is always the start_position.
# We don't need it in this example as it corresponds to the character's position.
path.remove(0)
set_process(true)

View File

@@ -5,7 +5,7 @@
[ext_resource path="res://character.gd" type="Script" id=3]
[ext_resource path="res://sprites/character.png" type="Texture" id=4]
[node name="Game" type="Node"]
[node name="Game" type="Node2D"]
[node name="TileMap" type="TileMap" parent="."]
tile_set = ExtResource( 1 )

View File

@@ -1,8 +1,8 @@
extends Position2D
export(float) var SPEED = 200.0
enum States { IDLE, FOLLOW }
enum STATES { IDLE, FOLLOW }
export(float) var speed = 200.0
var _state = null
var path = []
@@ -12,38 +12,35 @@ var target_position = Vector2()
var velocity = Vector2()
func _ready():
_change_state(STATES.IDLE)
func _change_state(new_state):
if new_state == STATES.FOLLOW:
path = get_parent().get_node('TileMap')._get_path(position, target_position)
if not path or len(path) == 1:
_change_state(STATES.IDLE)
return
# The index 0 is the starting cell
# we don't want the character to move back to it in this example
target_point_world = path[1]
_state = new_state
_change_state(States.IDLE)
func _process(_delta):
if not _state == STATES.FOLLOW:
if not _state == States.FOLLOW:
return
var arrived_to_next_point = move_to(target_point_world)
if arrived_to_next_point:
path.remove(0)
if len(path) == 0:
_change_state(STATES.IDLE)
_change_state(States.IDLE)
return
target_point_world = path[0]
func _input(event):
if event.is_action_pressed("click"):
if Input.is_key_pressed(KEY_SHIFT):
global_position = get_global_mouse_position()
else:
target_position = get_global_mouse_position()
_change_state(States.FOLLOW)
func move_to(world_position):
var MASS = 10.0
var ARRIVE_DISTANCE = 10.0
var desired_velocity = (world_position - position).normalized() * SPEED
var desired_velocity = (world_position - position).normalized() * speed
var steering = desired_velocity - velocity
velocity += steering / MASS
position += velocity * get_process_delta_time()
@@ -51,10 +48,13 @@ func move_to(world_position):
return position.distance_to(world_position) < ARRIVE_DISTANCE
func _input(event):
if event.is_action_pressed('click'):
if Input.is_key_pressed(KEY_SHIFT):
global_position = get_global_mouse_position()
else:
target_position = get_global_mouse_position()
_change_state(STATES.FOLLOW)
func _change_state(new_state):
if new_state == States.FOLLOW:
path = get_parent().get_node("TileMap")._get_path(position, target_position)
if not path or len(path) == 1:
_change_state(States.IDLE)
return
# The index 0 is the starting cell
# we don't want the character to move back to it in this example
target_point_world = path[1]
_state = new_state

View File

@@ -1,22 +1,22 @@
extends TileMap
# You can only create an AStar node from code, not from the Scene tab
onready var astar_node = AStar.new()
# The Tilemap node doesn't have clear bounds so we're defining the map's limits here
const BASE_LINE_WIDTH = 3.0
const DRAW_COLOR = Color.white
# The Tilemap node doesn't have clear bounds so we're defining the map's limits here.
export(Vector2) var map_size = Vector2(16, 16)
# The path start and end variables use setter methods
# You can find them at the bottom of the script
# The path start and end variables use setter methods.
# You can find them at the bottom of the script.
var path_start_position = Vector2() setget _set_path_start_position
var path_end_position = Vector2() setget _set_path_end_position
var _point_path = []
const BASE_LINE_WIDTH = 3.0
const DRAW_COLOR = Color('#fff')
# get_used_cells_by_id is a method from the TileMap node
# here the id 0 corresponds to the grey tile, the obstacles
# You can only create an AStar node from code, not from the Scene tab.
onready var astar_node = AStar.new()
# get_used_cells_by_id is a method from the TileMap node.
# Here the id 0 corresponds to the grey tile, the obstacles.
onready var obstacles = get_used_cells_by_id(0)
onready var _half_cell_size = cell_size / 2
@@ -25,123 +25,6 @@ func _ready():
astar_connect_walkable_cells(walkable_cells_list)
# Click and Shift force the start and end position of the path to update
# and the node to redraw everything
#func _input(event):
# if event.is_action_pressed('click') and Input.is_key_pressed(KEY_SHIFT):
# # To call the setter method from this script we have to use the explicit self.
# self.path_start_position = world_to_map(get_global_mouse_position())
# elif event.is_action_pressed('click'):
# self.path_end_position = world_to_map(get_global_mouse_position())
# Loops through all cells within the map's bounds and
# adds all points to the astar_node, except the obstacles
func astar_add_walkable_cells(obstacles = []):
var points_array = []
for y in range(map_size.y):
for x in range(map_size.x):
var point = Vector2(x, y)
if point in obstacles:
continue
points_array.append(point)
# The AStar class references points with indices
# Using a function to calculate the index from a point's coordinates
# ensures we always get the same index with the same input point
var point_index = calculate_point_index(point)
# AStar works for both 2d and 3d, so we have to convert the point
# coordinates from and to Vector3s
astar_node.add_point(point_index, Vector3(point.x, point.y, 0.0))
return points_array
# Once you added all points to the AStar node, you've got to connect them
# The points don't have to be on a grid: you can use this class
# to create walkable graphs however you'd like
# It's a little harder to code at first, but works for 2d, 3d,
# orthogonal grids, hex grids, tower defense games...
func astar_connect_walkable_cells(points_array):
for point in points_array:
var point_index = calculate_point_index(point)
# For every cell in the map, we check the one to the top, right.
# left and bottom of it. If it's in the map and not an obstalce,
# We connect the current point with it
var points_relative = PoolVector2Array([
Vector2(point.x + 1, point.y),
Vector2(point.x - 1, point.y),
Vector2(point.x, point.y + 1),
Vector2(point.x, point.y - 1)])
for point_relative in points_relative:
var point_relative_index = calculate_point_index(point_relative)
if is_outside_map_bounds(point_relative):
continue
if not astar_node.has_point(point_relative_index):
continue
# Note the 3rd argument. It tells the astar_node that we want the
# connection to be bilateral: from point A to B and B to A
# If you set this value to false, it becomes a one-way path
# As we loop through all points we can set it to false
astar_node.connect_points(point_index, point_relative_index, false)
# This is a variation of the method above
# It connects cells horizontally, vertically AND diagonally
func astar_connect_walkable_cells_diagonal(points_array):
for point in points_array:
var point_index = calculate_point_index(point)
for local_y in range(3):
for local_x in range(3):
var point_relative = Vector2(point.x + local_x - 1, point.y + local_y - 1)
var point_relative_index = calculate_point_index(point_relative)
if point_relative == point or is_outside_map_bounds(point_relative):
continue
if not astar_node.has_point(point_relative_index):
continue
astar_node.connect_points(point_index, point_relative_index, true)
func is_outside_map_bounds(point):
return point.x < 0 or point.y < 0 or point.x >= map_size.x or point.y >= map_size.y
func calculate_point_index(point):
return point.x + map_size.x * point.y
func _get_path(world_start, world_end):
self.path_start_position = world_to_map(world_start)
self.path_end_position = world_to_map(world_end)
_recalculate_path()
var path_world = []
for point in _point_path:
var point_world = map_to_world(Vector2(point.x, point.y)) + _half_cell_size
path_world.append(point_world)
return path_world
func _recalculate_path():
clear_previous_path_drawing()
var start_point_index = calculate_point_index(path_start_position)
var end_point_index = calculate_point_index(path_end_position)
# This method gives us an array of points. Note you need the start and end
# points' indices as input
_point_path = astar_node.get_point_path(start_point_index, end_point_index)
# Redraw the lines and circles from the start to the end point
update()
func clear_previous_path_drawing():
if not _point_path:
return
var point_start = _point_path[0]
var point_end = _point_path[len(_point_path) - 1]
set_cell(point_start.x, point_start.y, -1)
set_cell(point_end.x, point_end.y, -1)
func _draw():
if not _point_path:
return
@@ -159,6 +42,121 @@ func _draw():
last_point = current_point
# Click and Shift force the start and end position of the path to update,
# and the node to redraw everything.
#func _input(event):
# if event.is_action_pressed('click') and Input.is_key_pressed(KEY_SHIFT):
# # To call the setter method from this script we have to use the explicit self.
# self.path_start_position = world_to_map(get_global_mouse_position())
# elif event.is_action_pressed('click'):
# self.path_end_position = world_to_map(get_global_mouse_position())
# Loops through all cells within the map's bounds and
# adds all points to the astar_node, except the obstacles.
func astar_add_walkable_cells(obstacle_list = []):
var points_array = []
for y in range(map_size.y):
for x in range(map_size.x):
var point = Vector2(x, y)
if point in obstacle_list:
continue
points_array.append(point)
# The AStar class references points with indices.
# Using a function to calculate the index from a point's coordinates
# ensures we always get the same index with the same input point.
var point_index = calculate_point_index(point)
# AStar works for both 2d and 3d, so we have to convert the point
# coordinates from and to Vector3s.
astar_node.add_point(point_index, Vector3(point.x, point.y, 0.0))
return points_array
# Once you added all points to the AStar node, you've got to connect them.
# The points don't have to be on a grid: you can use this class
# to create walkable graphs however you'd like.
# It's a little harder to code at first, but works for 2d, 3d,
# orthogonal grids, hex grids, tower defense games...
func astar_connect_walkable_cells(points_array):
for point in points_array:
var point_index = calculate_point_index(point)
# For every cell in the map, we check the one to the top, right.
# left and bottom of it. If it's in the map and not an obstalce.
# We connect the current point with it.
var points_relative = PoolVector2Array([
Vector2(point.x + 1, point.y),
Vector2(point.x - 1, point.y),
Vector2(point.x, point.y + 1),
Vector2(point.x, point.y - 1)])
for point_relative in points_relative:
var point_relative_index = calculate_point_index(point_relative)
if is_outside_map_bounds(point_relative):
continue
if not astar_node.has_point(point_relative_index):
continue
# Note the 3rd argument. It tells the astar_node that we want the
# connection to be bilateral: from point A to B and B to A.
# If you set this value to false, it becomes a one-way path.
# As we loop through all points we can set it to false.
astar_node.connect_points(point_index, point_relative_index, false)
# This is a variation of the method above.
# It connects cells horizontally, vertically AND diagonally.
func astar_connect_walkable_cells_diagonal(points_array):
for point in points_array:
var point_index = calculate_point_index(point)
for local_y in range(3):
for local_x in range(3):
var point_relative = Vector2(point.x + local_x - 1, point.y + local_y - 1)
var point_relative_index = calculate_point_index(point_relative)
if point_relative == point or is_outside_map_bounds(point_relative):
continue
if not astar_node.has_point(point_relative_index):
continue
astar_node.connect_points(point_index, point_relative_index, true)
func calculate_point_index(point):
return point.x + map_size.x * point.y
func clear_previous_path_drawing():
if not _point_path:
return
var point_start = _point_path[0]
var point_end = _point_path[len(_point_path) - 1]
set_cell(point_start.x, point_start.y, -1)
set_cell(point_end.x, point_end.y, -1)
func is_outside_map_bounds(point):
return point.x < 0 or point.y < 0 or point.x >= map_size.x or point.y >= map_size.y
func _get_path(world_start, world_end):
self.path_start_position = world_to_map(world_start)
self.path_end_position = world_to_map(world_end)
_recalculate_path()
var path_world = []
for point in _point_path:
var point_world = map_to_world(Vector2(point.x, point.y)) + _half_cell_size
path_world.append(point_world)
return path_world
func _recalculate_path():
clear_previous_path_drawing()
var start_point_index = calculate_point_index(path_start_position)
var end_point_index = calculate_point_index(path_end_position)
# This method gives us an array of points. Note you need the start and
# end points' indices as input.
_point_path = astar_node.get_point_path(start_point_index, end_point_index)
# Redraw the lines and circles from the start to the end point.
update()
# Setters for the start and end path values.
func _set_path_start_position(value):
if value in obstacles:

View File

@@ -4,7 +4,7 @@
[ext_resource path="res://sprites/path_start.png" type="Texture" id=2]
[ext_resource path="res://sprites/path_end.png" type="Texture" id=3]
[node name="Node2D" type="Node2D"]
[node name="Tileset" type="Node2D"]
[node name="Obstacle" type="Sprite" parent="."]
position = Vector2( 32, 32 )
@@ -17,4 +17,3 @@ texture = ExtResource( 2 )
[node name="PathEnd" type="Sprite" parent="."]
position = Vector2( 192, 32 )
texture = ExtResource( 3 )