Help me understand instantiating mobs and changing their parameters

Godot Version

v4.5.2.stable.steam.6ce3de25a

Question

Hello,

I am trying to take my mob scene, instantiate it, add it as a child of the scene, and make each mob I spawn be a different color. The mob scene consists of a RigidBody2D with two children: AnimatedSprite2D and CollisionShape2D.

I assumed that shaders would be the easiest method to make each AnimatedSprite2D a different color, so I read the beginner tutorial and made a shader material on AnimatedSprite2D and added uniforms “red”, “green”, and “blue”. Should I be using this method?

After I instantiate the packed scene, do I access the uniforms then, or after I add it as a child? Once I add a few instances of the same scene as children, how do I know how they will be named so I can access the most recent one and change its parameters before the player sees it? Is there a way to reliably find out how to access the parameters of my nodes once they enter the scene tree?

Thank you for helping me out. I’ve watched a lot of tutorials but still feel like I have big gaps and don’t know what they even are yet.

You can use a material with your own shader without problem. Do make note of the fact that materials are shared between instances which means that setting the uniform of the material of one of the sprites will change the color for all instances using that same material. To support per-instance uniform values, you should make use of the instance uniform type in your shader. The uniform can then be set via CanvasItem’s set_instance_shader_parameter() method.


That said, it is much simpler to just set the CanvasItem’s modulate value instead which tints the color of the sprite. However, if you want to use a shader, that’s good too. It depends on what you want to make. Here is an example I made using modulate:

EnemySpawner.gd
extends Node2D

@export var amount_to_spawn = 5
@export var enemy_prefab: PackedScene

func _input(event: InputEvent) -> void:
	if event is InputEventKey:
		if event.pressed and event.keycode == KEY_SPACE:
			spawn_enemies()
			
func spawn_enemies():
	for i in range(amount_to_spawn):
		# Instantiate and set random color
		var instance = enemy_prefab.instantiate()
		var color = Color.from_hsv(randf(), 1, 1, 1) # Color with random hue
		instance.set_color(color)
		
		# Compute random position from spawner
		var random = (randf() * 2.0 - 1.0)	# Random number in [-1, 1] range
		var spawn_pos = global_position + Vector2.RIGHT * random * 200.0
		
		# Add instance to scene tree and set its position
		add_child(instance)
		instance.global_position = spawn_pos

EnemyController.gd
extends RigidBody2D

@export var sprite_path = "Sprite2D"

func set_color(color: Color):
	get_node(sprite_path).modulate = color

GodotChangingColorEnemySpawnExample

Without going too much into detail, as this is a frequently taught OOP concept, an EnemyController script is used to encapsulate enemy-specific systems and methods in its own class. The EnemySpawner then uses the enemy’s change_color() method to… change its color.

The important thing to understand is that any paths that relate to the inner structure of the enemy is kept within the enemy’s class. You can imagine having several different types of enemies, each with their own node tree structure where the path to the color-changing sprite isn’t the same. If you were to let the EnemySpawner evaluate the location of the Sprite2D’s location in the enemy’s node hierarchy, you will quickly end up with spawner code specialized for spawning enemies of various types rather than the class exclusively implementing generalized spawning logic.

As a rule of thumb, anything under an object’s root should not be touched by another system. This is also reflected in Godot’s scene system which hides underlying nodes of any scene instance.

Differing enemy node tree examples

Enemy 1 Node Tree:

  • RigidBody2D
    • AnimatedSprite2D

hypothetical example

Enemy 2 Node Tree:

  • RigidBody2D
    • Rotator
      • AnimatedSprite2D

hypothetical example


It doesn’t matter for uniforms. The only time the execution order matters, in the context of adding nodes to the tree, is when the tree affects the variable in question. This is the case for a node’s transform (position, rotation, scale) which depends on the parent it is a child of. Rendering stuff is sometimes affected as well. You just have to look it up or test it.


If you want to keep track of your enemy instances, you can store the instances in an Array somewhere for later reference. A common method is to make use of a singleton (usually an Autoload) which stores globally accessible variables.


Should you have any further questions, let me know.