Dynamic obstacles
Three obstacles stepping in and out of the navigation mesh. Every swap rebakes the region and the agent takes whichever lane is still open.
The editor above is running this project. Change a line and it reloads.
Godot classes used
NavigationObstacle2DNavigationRegion2DNavigationAgent2D
The code
scripts/main.gd
extends Node
const SPEED: float = 330.0
const BODY_RADIUS: float = 16.0
const MARGIN: float = 60.0
const GAP: float = 40.0
const SWAP_EVERY: float = 1.2
const COLUMN_CLEAR: float = 120.0
var bounds: Rect2 = Rect2()
var column_x: float = 0.0
var poly: NavigationPolygon
var region: NavigationRegion2D
var region_visual: Node2D
var obstacle_visuals: Array[Polygon2D] = []
var obstacles: Array[NavigationObstacle2D] = []
var open_index: int = 1
var swap_timer: float = 0.0
var bakes: int = 0
var info: Label
var goals: PackedVector2Array = PackedVector2Array()
var goal_index: int = 0
var body: Node2D
var agent: NavigationAgent2D
var path_line: Line2D
func _ready() -> void:
_add_background()
var view: Vector2 = get_viewport().get_visible_rect().size
bounds = Rect2(MARGIN, MARGIN, view.x - MARGIN * 2.0, view.y - MARGIN * 2.0)
column_x = view.x * 0.5
goals = PackedVector2Array([Vector2(view.x - 130, view.y * 0.5), Vector2(130, view.y * 0.5)])
poly = NavigationPolygon.new()
poly.agent_radius = BODY_RADIUS + 8.0
# with the default PARSED_GEOMETRY_BOTH every Polygon2D on screen is parsed as an obstruction
poly.parsed_geometry_type = NavigationPolygon.PARSED_GEOMETRY_STATIC_COLLIDERS
region = NavigationRegion2D.new()
add_child(region)
var slots: Array[Rect2] = []
var span: float = (bounds.size.y - GAP * 2.0) / 3.0
for i in 3:
slots.append(Rect2(column_x - 45.0, MARGIN + i * (span + GAP), 90, span))
for slot in slots:
var obstacle: NavigationObstacle2D = NavigationObstacle2D.new()
obstacle.position = slot.position + slot.size * 0.5
obstacle.vertices = _rect_points(Rect2(-slot.size * 0.5, slot.size))
add_child(obstacle)
obstacles.append(obstacle)
region_visual = Node2D.new()
add_child(region_visual)
for slot in slots:
var block: Polygon2D = _make_rect(slot.size, Color('#fc7f7f'))
block.position = slot.position + slot.size * 0.5
add_child(block)
obstacle_visuals.append(block)
path_line = Line2D.new()
path_line.width = 5.0
path_line.default_color = Color('#8eef97')
add_child(path_line)
body = Node2D.new()
body.position = goals[1]
body.add_child(_make_circle(BODY_RADIUS, Color('#8da5f3')))
add_child(body)
agent = NavigationAgent2D.new()
agent.radius = BODY_RADIUS
agent.path_desired_distance = 12.0
agent.target_desired_distance = 12.0
body.add_child(agent)
info = Label.new()
info.position = Vector2(MARGIN, 18)
info.add_theme_font_size_override('font_size', 20)
info.add_theme_color_override('font_color', Color('#f3d48d'))
add_child(info)
_apply_obstacles()
print('one of the three obstacles steps out every %.1fs and the agent re-routes through the gap' % SWAP_EVERY)
func _physics_process(delta: float) -> void:
swap_timer += delta
if swap_timer >= SWAP_EVERY and absf(body.position.x - column_x) > COLUMN_CLEAR:
swap_timer = 0.0
open_index = (open_index + 1) % obstacles.size()
_apply_obstacles()
var next: Vector2 = agent.get_next_path_position()
var path: PackedVector2Array = agent.get_current_navigation_path()
if path.is_empty():
agent.target_position = goals[goal_index]
return
path_line.points = path
if agent.is_navigation_finished():
goal_index = (goal_index + 1) % goals.size()
agent.target_position = goals[goal_index]
return
body.position = body.position.move_toward(next, SPEED * delta)
func _apply_obstacles() -> void:
for i in obstacles.size():
obstacles[i].affect_navigation_mesh = i != open_index
obstacle_visuals[i].color = Color(0.99, 0.5, 0.5, 0.16) if i == open_index else Color('#fc7f7f')
_rebake()
agent.target_position = goals[goal_index]
func _rebake() -> void:
var source: NavigationMeshSourceGeometryData2D = NavigationMeshSourceGeometryData2D.new()
# parse_source_geometry_data wipes the data it receives, so the traversable outline comes after it
NavigationServer2D.parse_source_geometry_data(poly, source, self)
source.add_traversable_outline(_rect_points(bounds))
NavigationServer2D.bake_from_source_geometry_data(poly, source)
region.navigation_polygon = poly
_rebuild_region_visual()
bakes += 1
info.text = 'bake %d: %d obstacles in the mesh, %d navigation polygons' % [bakes, obstacles.size() - 1, poly.get_polygon_count()]
func _rebuild_region_visual() -> void:
for face in region_visual.get_children():
face.queue_free()
var vertices: PackedVector2Array = poly.get_vertices()
for i in poly.get_polygon_count():
var points: PackedVector2Array = PackedVector2Array()
for index in poly.get_polygon(i):
points.append(vertices[index])
var face: Polygon2D = Polygon2D.new()
face.polygon = points
face.color = Color(0.55, 0.65, 0.95, 0.12)
region_visual.add_child(face)
var edge: Line2D = Line2D.new()
edge.points = points
edge.closed = true
edge.width = 2.0
edge.default_color = Color(0.55, 0.65, 0.95, 0.45)
region_visual.add_child(edge)
func _rect_points(rect: Rect2) -> PackedVector2Array:
return PackedVector2Array([rect.position, Vector2(rect.end.x, rect.position.y), rect.end, Vector2(rect.position.x, rect.end.y)])
func _make_rect(size: Vector2, color: Color) -> Polygon2D:
var rect: Polygon2D = Polygon2D.new()
rect.polygon = PackedVector2Array([-size * 0.5, Vector2(size.x, -size.y) * 0.5, size * 0.5, Vector2(-size.x, size.y) * 0.5])
rect.color = color
return rect
func _make_circle(radius: float, color: Color) -> Polygon2D:
var points: PackedVector2Array = PackedVector2Array()
for i in 20:
points.append(Vector2(cos(TAU * i / 20.0), sin(TAU * i / 20.0)) * radius)
var circle: Polygon2D = Polygon2D.new()
circle.polygon = points
circle.color = color
return circle
func _add_background() -> void:
var background: ColorRect = ColorRect.new()
background.color = Color('#12141a')
add_child(background)
background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)