Application material to selected character

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.

What are you trying to achieve exactly? can you post an image of how the contour works?

Writing a shader for a contour is quite easy, and you can just set a shader parameter for a contour off/on.
I think by changing the materials and duplicating materials everything gets more complicated than what it should.

Could you share some screenshots of what your trying to achieve? Basically if you select 3 characters you want those characters to be highlighted?

1 Like

What I want to make is a unit management system like in RTS games. Through the Control selection area, you select the units you want and guide them by clicking on the surface with a navigation grid. The shader material is simply meant to outline the circle of characters that are currently selected by the player.


This is a screenshot from the editor showing the main scene, just to give you an idea, as you can see there are 3 identical characters, all of them in a special group to be considered as “characters”.


This video demonstrates my problem. All the characters are identical in terms of characteristics because they are based on the same scene. As you can see, the system works, but as soon as I add a character to the selection that is to the left of all the characters at the beginning of the game, the selection material is added to all the characters at once, despite the fact that only 1 is selected, and I even demonstrated this by giving a move order. The only pattern that I understood is that the last character added to the scene causes this error, as I described in the examples at the end of the main post. If you delete the leftmost character, the error with the material will be caused by the character on the right because it is considered the second character added to the scene and after removing the previous one, it is considered the last.
I’m probably overthinking my implementation of this system, so I’d appreciate any help.

I see.
Maybe the problem is that when you define or assign your material you have to activate “Local to scene”. Otherwise, any change to the material will apply to all objects that have it.

image

2 Likes

Yes, you’re right, I had an idea that all characters share 1 material at the same time, checking the box helped to fix the problem with the material.
You have apparently looked at my code. What tips can you give me on how to optimise it or improve it?

Your code pretty good, I would have done mostly the same. However I’d have solved that with shaders instead of adding and removing materials. But it’s ok, it’s the approach you found and it’s ok, seriously.

For instance, here. I have 3 characters from the same scene. All with one material and one shader. All with “Local to scene” on, so the highlight is independent from each other and I don’t run into the same issue you had. But I never change the material or the shader. I wrote a super basic shader that adds a contour to the selected character. And I press left arrow, up arrow and right arrow to select and deselect them independently (I didn’t have time to code a selection box, but you get the idea).

In the shader, I added a parameter (uniform) called “show_contour” and if it’s on, I just show the contour, otherwise I don’t.
I never change the material, it’s the same material and shader for all of them.

Check this out:

And this is the shader. Again, super basic (and kinda sloppy, it’s just for this example) but it works and I think it gets the point accross. If show_contour is true, it checks if the current pixel is transparent and an adyacent pixel is not, so it paints the current pixel white.

shader_type canvas_item;

uniform bool show_contour = false;

void fragment() {
	
	vec4 color = texture(TEXTURE, UV);
	vec4 color_l = texture(TEXTURE, UV - vec2(-0.01, 0.00));
	vec4 color_r = texture(TEXTURE, UV - vec2( 0.01, 0.00));
	vec4 color_u = texture(TEXTURE, UV - vec2( 0.00, -0.01));
	vec4 color_d = texture(TEXTURE, UV - vec2( 0.00, 0.01));
	
	if (show_contour){
		if (color.a == 0.0){
			if ((color_l.a > 0.0) || (color_r.a > 0.0) || (color_u.a > 0.0) || (color_d.a > 0.0)){
				COLOR = vec4(1,1,1,1);
			}
		}
	}
}

I’m not familiar with 3D shaders yet, however for 2D it’s just a simple shader and setting the shader property to true or false. It should be somewhat similar in 3D.

But again, if you did it with materials it’s fine too, it works, keep going with the project. Maybe next time if you look into shaders you can solve it in less time and with less problems.
Good luck!

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.