Agente 3D

La misma idea en tres dimensiones, con la NavigationMesh generada de mallas primitivas al cargar. Haz clic en el suelo para elegir destino.

El editor de arriba está ejecutando este proyecto. Cambia una línea y se recarga.

Clases de Godot usadas

NavigationRegion3DNavigationAgent3DCharacterBody3D

El código

scripts/main.gd
extends Node

const SPEED: float = 9.0
const GROUND: float = 30.0
const AGENT_RADIUS: float = 0.75
const DOT_COUNT: int = 80
const DOT_SPACING: float = 1.0

var walls: Array[AABB] = [
	AABB(Vector3(-8, 0, -13), Vector3(2, 3, 20)),
	AABB(Vector3(-1, 0, -7), Vector3(2, 3, 20)),
	AABB(Vector3(6, 0, -13), Vector3(2, 3, 20)),
]
var targets: Array[Vector3] = [Vector3(12, 0, 12), Vector3(12, 0, -12), Vector3(-12, 0, 12), Vector3(-12, 0, -12)]
var target_index: int = 0

var player: CharacterBody3D
var agent: NavigationAgent3D
var camera: Camera3D
var marker: MeshInstance3D
var dots: Array[MeshInstance3D] = []


func _ready() -> void:
	_add_environment()
	_add_light()
	_add_ground()

	for wall in walls:
		_add_wall(wall)

	var region: NavigationRegion3D = NavigationRegion3D.new()
	region.navigation_mesh = _bake()
	add_child(region)

	_add_navmesh_visual(region.navigation_mesh)
	_add_dots()

	marker = MeshInstance3D.new()
	var disc: CylinderMesh = CylinderMesh.new()
	disc.top_radius = 0.9
	disc.bottom_radius = 0.9
	disc.height = 0.1
	marker.mesh = disc
	marker.material_override = _flat(Color('#f3d48d'))
	add_child(marker)

	player = CharacterBody3D.new()
	player.position = Vector3(-12, 0, -12)
	add_child(player)
	_dress_player()

	agent = NavigationAgent3D.new()
	agent.radius = AGENT_RADIUS
	# the baked mesh floats one cell above the floor, and a desired distance under that gap stalls the agent on its first path point
	agent.path_desired_distance = 1.0
	agent.target_desired_distance = 1.2
	agent.path_height_offset = 0.5
	player.add_child(agent)

	camera = Camera3D.new()
	camera.position = Vector3(0, 20, 21)
	add_child(camera)
	camera.look_at(Vector3.ZERO, Vector3.UP)
	camera.current = true

	print('the walker tours the four corners, click the floor to send it there instead')


func _physics_process(_delta: float) -> void:
	var next: Vector3 = agent.get_next_path_position()
	var path: PackedVector3Array = agent.get_current_navigation_path()

	# the navigation map only syncs at the end of a physics frame, so the first queries come back empty
	if path.is_empty():
		_send_to(targets[target_index])
		return

	_update_dots(path)

	if agent.is_navigation_finished():
		target_index = (target_index + 1) % targets.size()
		_send_to(targets[target_index])
		return

	var direction: Vector3 = next - player.global_position
	direction.y = 0.0

	if direction.length() > 0.05:
		player.velocity = direction.normalized() * SPEED
		player.look_at(player.global_position + direction, Vector3.UP)

	player.move_and_slide()


func _unhandled_input(event: InputEvent) -> void:
	if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
		var hit: Variant = Plane(Vector3.UP, 0.0).intersects_ray(camera.project_ray_origin(event.position), camera.project_ray_normal(event.position))

		if hit != null:
			_send_to(hit)


func _send_to(where: Vector3) -> void:
	agent.target_position = where
	marker.position = where + Vector3(0, 0.1, 0)


func _bake() -> NavigationMesh:
	var nav: NavigationMesh = NavigationMesh.new()
	nav.cell_size = 0.25
	nav.cell_height = 0.25
	nav.agent_radius = AGENT_RADIUS
	nav.agent_height = 1.5
	nav.agent_max_climb = 0.25

	var source: NavigationMeshSourceGeometryData3D = NavigationMeshSourceGeometryData3D.new()

	var floor_mesh: PlaneMesh = PlaneMesh.new()
	floor_mesh.size = Vector2(GROUND, GROUND)
	# add_mesh would pull the geometry back from the GPU, add_mesh_array keeps it on the cpu
	source.add_mesh_array(floor_mesh.get_mesh_arrays(), Transform3D.IDENTITY)

	for wall in walls:
		var box: BoxMesh = BoxMesh.new()
		box.size = wall.size
		source.add_mesh_array(box.get_mesh_arrays(), Transform3D(Basis.IDENTITY, wall.position + wall.size * 0.5))

	NavigationServer3D.bake_from_source_geometry_data(nav, source)

	return nav


func _add_navmesh_visual(nav: NavigationMesh) -> void:
	var vertices: PackedVector3Array = nav.get_vertices()
	var faces: PackedVector3Array = PackedVector3Array()

	for i in nav.get_polygon_count():
		var indices: PackedInt32Array = nav.get_polygon(i)

		for corner in range(1, indices.size() - 1):
			faces.append(vertices[indices[0]])
			faces.append(vertices[indices[corner]])
			faces.append(vertices[indices[corner + 1]])

	var arrays: Array = []
	arrays.resize(Mesh.ARRAY_MAX)
	arrays[Mesh.ARRAY_VERTEX] = faces

	var mesh: ArrayMesh = ArrayMesh.new()
	mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)

	var material: StandardMaterial3D = _flat(Color(0.55, 0.65, 0.95, 0.4))
	material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
	material.cull_mode = BaseMaterial3D.CULL_DISABLED

	var visual: MeshInstance3D = MeshInstance3D.new()
	visual.mesh = mesh
	visual.material_override = material
	add_child(visual)


func _add_dots() -> void:
	var sphere: SphereMesh = SphereMesh.new()
	sphere.radius = 0.16
	sphere.height = 0.32
	sphere.radial_segments = 6
	sphere.rings = 3

	var material: StandardMaterial3D = _flat(Color('#8eef97'))

	for i in DOT_COUNT:
		var dot: MeshInstance3D = MeshInstance3D.new()
		dot.mesh = sphere
		dot.material_override = material
		dot.visible = false
		add_child(dot)
		dots.append(dot)


func _update_dots(path: PackedVector3Array) -> void:
	var used: int = 0

	for i in range(path.size() - 1):
		var steps: int = int(path[i].distance_to(path[i + 1]) / DOT_SPACING)

		for step in steps:
			if used >= dots.size():
				break

			dots[used].position = path[i].lerp(path[i + 1], float(step) / float(steps)) + Vector3(0, 0.2, 0)
			dots[used].visible = true
			used += 1

	for i in range(used, dots.size()):
		dots[i].visible = false


func _dress_player() -> void:
	var capsule: CapsuleMesh = CapsuleMesh.new()
	capsule.radius = 0.5
	capsule.height = 2.0

	var visual: MeshInstance3D = MeshInstance3D.new()
	visual.mesh = capsule
	visual.position = Vector3(0, 1, 0)
	visual.material_override = _lit(Color('#8da5f3'))
	player.add_child(visual)

	var nose: MeshInstance3D = MeshInstance3D.new()
	var box: BoxMesh = BoxMesh.new()
	box.size = Vector3(0.3, 0.3, 0.7)
	nose.mesh = box
	nose.position = Vector3(0, 1.2, -0.6)
	nose.material_override = _lit(Color('#f3d48d'))
	player.add_child(nose)

	var shape: CapsuleShape3D = CapsuleShape3D.new()
	shape.radius = 0.5
	shape.height = 2.0

	var collision: CollisionShape3D = CollisionShape3D.new()
	collision.shape = shape
	collision.position = Vector3(0, 1, 0)
	player.add_child(collision)


func _add_wall(wall: AABB) -> void:
	var mesh: BoxMesh = BoxMesh.new()
	mesh.size = wall.size

	var visual: MeshInstance3D = MeshInstance3D.new()
	visual.mesh = mesh
	visual.material_override = _lit(Color('#fc7f7f'))

	var shape: BoxShape3D = BoxShape3D.new()
	shape.size = wall.size

	var collision: CollisionShape3D = CollisionShape3D.new()
	collision.shape = shape

	var body: StaticBody3D = StaticBody3D.new()
	body.position = wall.position + wall.size * 0.5
	body.add_child(visual)
	body.add_child(collision)
	add_child(body)


func _add_ground() -> void:
	var mesh: BoxMesh = BoxMesh.new()
	mesh.size = Vector3(GROUND, 1, GROUND)

	var visual: MeshInstance3D = MeshInstance3D.new()
	visual.mesh = mesh
	visual.material_override = _lit(Color('#2a2f3d'))

	var shape: BoxShape3D = BoxShape3D.new()
	shape.size = mesh.size

	var collision: CollisionShape3D = CollisionShape3D.new()
	collision.shape = shape

	var body: StaticBody3D = StaticBody3D.new()
	body.position = Vector3(0, -0.5, 0)
	body.add_child(visual)
	body.add_child(collision)
	add_child(body)


func _lit(color: Color) -> StandardMaterial3D:
	var material: StandardMaterial3D = StandardMaterial3D.new()
	material.albedo_color = color
	material.roughness = 0.6

	return material


func _flat(color: Color) -> StandardMaterial3D:
	var material: StandardMaterial3D = StandardMaterial3D.new()
	material.albedo_color = color
	material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED

	return material


func _add_environment() -> void:
	var sky_material: ProceduralSkyMaterial = ProceduralSkyMaterial.new()
	sky_material.sky_top_color = Color(0.05, 0.06, 0.12)
	sky_material.sky_horizon_color = Color(0.14, 0.16, 0.24)
	sky_material.ground_bottom_color = Color(0.06, 0.07, 0.11)
	sky_material.ground_horizon_color = Color(0.1, 0.11, 0.16)

	var sky: Sky = Sky.new()
	sky.sky_material = sky_material

	var env: Environment = Environment.new()
	env.background_mode = Environment.BG_SKY
	env.sky = sky
	env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
	env.ambient_light_energy = 0.7

	var world: WorldEnvironment = WorldEnvironment.new()
	world.environment = env
	add_child(world)


func _add_light() -> void:
	var light: DirectionalLight3D = DirectionalLight3D.new()
	light.rotation_degrees = Vector3(-55, -35, 0)
	light.light_energy = 1.2
	light.shadow_enabled = true
	add_child(light)

Más ejemplos de Navegación