I created a mechanism in which I have characters on the stage and I can select one or more of them and order them to go to a certain point using pathfinding.
The main stage includes:
SelectionBox - is a Control node used to draw an area through which characters are selected.
Character - is a separate scene that contains a CharacterBody3d and a script attached to it for moving it, as well as methods that mark this character as active and perform certain manipulations.
SelectionBox code:
extends Control
var is_visible = false
var m_pos = Vector2()
var start_sel_pos = Vector2()
const sel_box_col = Color(0, 1, 0, 0.5) # Use a semi-transparent color for better visibility
const sel_box_line_width = 3
# Store references to selected characters
var selected_characters = []
# Reference to the camera
@onready var camera = get_tree().get_nodes_in_group("Camera")[0] # Adjust the path according to your scene tree
func _ready():
# Ensure the camera is set as the current camera
if camera:
camera.current = true
else:
print("Camera is not found!")
func _draw():
if is_visible and start_sel_pos != m_pos:
# Draw the selection box
draw_line(start_sel_pos, Vector2(m_pos.x, start_sel_pos.y), sel_box_col, sel_box_line_width)
draw_line(start_sel_pos, Vector2(start_sel_pos.x, m_pos.y), sel_box_col, sel_box_line_width)
draw_line(m_pos, Vector2(m_pos.x, start_sel_pos.y), sel_box_col, sel_box_line_width)
draw_line(m_pos, Vector2(start_sel_pos.x, m_pos.y), sel_box_col, sel_box_line_width)
func _process(delta):
if is_visible:
queue_redraw() # Only request redraw when selection box is visible
func _input(event):
if event is InputEventMouseButton:
if event.button_index == MouseButton.MOUSE_BUTTON_LEFT:
if event.pressed:
# Capture start position on mouse button press
start_sel_pos = event.position
m_pos = start_sel_pos # Initialize current position to start position
is_visible = true # Show selection box
else:
# When the mouse button is released
is_visible = false # Hide selection box
select_characters_in_box()
clear_selection_box() # Clear selection box after selection
if event is InputEventMouseMotion and is_visible:
# Update current mouse position while dragging
m_pos = event.position
func select_characters_in_box():
selected_characters.clear() # Clear previously selected characters
var selection_rect = Rect2(start_sel_pos, m_pos - start_sel_pos).abs() # Calculate the selection rectangle
for character in get_tree().get_nodes_in_group("Characters"):
if camera:
# Convert character's 3D position to 2D screen position
var char_screen_pos = camera.unproject_position(character.global_position)
if selection_rect.has_point(char_screen_pos):
selected_characters.append(character)
character.set_selected(true) # Assuming this method exists in the character script
else:
character.set_selected(false)
print("Selected characters:", selected_characters)
func clear_selection_box():
is_visible = false
queue_redraw() # Ensure the selection box is not drawn anymore
Character code:
extends CharacterBody3D
@onready var navigationAgent: NavigationAgent3D = $NavigationAgent3D
@onready var body = $Body # Reference to the body MeshInstance3D
var Speed = 8
var is_selected = false
var has_target = false # Track if the character has a movement target
# Load the selection shader material
var selection_material: ShaderMaterial = preload("res://shaders/material/selections_material.tres")
func _ready():
# Initialize the character settings if needed
pass
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
if not has_target:
return
if navigationAgent.is_navigation_finished():
has_target = false # Reset target when navigation is finished
return
moveToPoint(delta, Speed)
func moveToPoint(delta, speed):
var targetPos = navigationAgent.get_next_path_position()
var direction = global_position.direction_to(targetPos)
faceDirection(targetPos)
velocity = direction * speed
move_and_slide()
func faceDirection(direction):
look_at(Vector3(direction.x, global_position.y, direction.z), Vector3.UP)
func _input(event):
if Input.is_action_just_pressed("LeftMouse") and is_selected:
var camera = get_tree().get_nodes_in_group("Camera")[0]
var mousePos = get_viewport().get_mouse_position()
var rayLength = 100
var from = camera.project_ray_origin(mousePos)
var to = from + camera.project_ray_normal(mousePos) * rayLength
var space = get_world_3d().direct_space_state
var rayQuery = PhysicsRayQueryParameters3D.new()
rayQuery.from = from
rayQuery.to = to
rayQuery.collide_with_areas = true
var result = space.intersect_ray(rayQuery)
if result: # Check if something was hit
navigationAgent.target_position = result.position
has_target = true # Set target when a valid position is found
else:
print("No valid target position found")
# Method to update selection state
func set_selected(selected: bool):
is_selected = selected
if is_selected:
print("Add material:", body)
apply_selection_material()
else:
print("Remove material:", body)
remove_selection_material()
# Method to apply the selection material as a next_pass
func apply_selection_material():
# Ensure the body has a material_override
if body.material_override == null:
body.material_override = StandardMaterial3D.new() # Create a new base material if none exists
var existing_material = body.material_override
# Check if the existing material is a ShaderMaterial or not
if existing_material is ShaderMaterial:
# Duplicate the existing shader material to apply next_pass
var shader_material_instance = existing_material.duplicate() as ShaderMaterial
shader_material_instance.next_pass = selection_material
body.material_override = shader_material_instance
else:
# If it's not a ShaderMaterial, assign the selection_material as next_pass
existing_material.next_pass = selection_material
# Method to remove the selection material
func remove_selection_material():
if body.material_override:
var existing_material = body.material_override
# Remove the next_pass material if it matches the selection material
if existing_material.next_pass == selection_material:
existing_material.next_pass = null
The main problem is that all selected characters receive a shader material on top of their main material, which creates a contour. But the code doesn’t work very stable if you place the logging methods in key places, everything seems to be fine, but if there is more than 1 character in the scene, the shader material is displayed only when the last character added to the scene is in our selection, and even if you select only one of them, the selection material is added to all the others. An example of an error: There are 2 characters in the scene (Character, Character2) When you select only Character2, the material appears in Character. There are 3 characters in the scene (Character, Character2, Character3) When you select Character3 alone, the material appears in both Character and Character2. When selecting other characters except the last one added, everything works fine except that the shader material does not appear.