Can you write this from memory?
Add the current node to the 'enemies' group so it can be found later.
Groups are Godot's "tag" system. They let you label nodes (player, enemy, pickup, projectile) and then query or broadcast behavior without hard-coding node paths. Use groups to build pickups, triggers, and "affect all enemies" events without brittle node paths.
Combined with collision detection, groups power most pickup systems. The pattern: detect the overlap → check is_in_group("player") → apply effect → queue_free().
Groups decouple your systems. A weapon doesn't need to know every enemy type—it just damages anything in the "enemy" group. A pickup doesn't need a direct player reference—it heals anything in the "player" group.
# Without groups: brittle, breaks if you rename/add enemies
if body.name == "Goblin" or body.name == "Skeleton":
body.take_damage(10)
# With groups: flexible, scales to any number of enemy types
if body.is_in_group(&"enemy"):
body.take_damage(10)
You don't always need code to assign groups:
- Node dock method: Select a node → Node tab (next to Inspector) → Groups section → type a name and click Add
- Global Groups (Project Settings): Project → Project Settings → Global Groups → define groups here, then assign them to any node via the Node dock dropdown
Global Groups are useful for project-wide consistency—define player, enemy, pickup once, and they appear in every node's group dropdown.
This is the core pattern for coins, health packs, power-ups, and collectibles:
extends Area2D
@export var heal_amount := 25
const GROUP_PLAYER := &"player"
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node) -> void:
if not body.is_in_group(GROUP_PLAYER):
return
# If the player implements heal(), this works.
# call() ignores nodes that don't have the method.
body.call(&"heal", heal_amount)
queue_free()
Key insight: Groups are not a replacement for collision layers/masks—they're a second filter. Use layers/masks to control which physics objects can overlap, then use groups to identify what role that object plays ("is this a player?").
# Freeze all enemies (runs IMMEDIATELY in Godot 4)
get_tree().call_group(&"enemy", &"freeze", 2.0)
# Heal all allies
get_tree().call_group(&"ally", &"heal", 50)
# Pause all AI
get_tree().call_group(&"ai", &"set_active", false)
Godot 4 behavior: call_group() acts immediately—not end-of-frame. Nodes that don't have the method are silently ignored (no error). If you need end-of-frame timing:
# Deferred call_group (runs at end of frame, like call_deferred)
get_tree().call_group_flags(
SceneTree.GROUP_CALL_DEFERRED,
&"enemy",
&"freeze",
2.0
)
# Find all enemies
var enemies := get_tree().get_nodes_in_group(&"enemy")
Find Closest Enemy (Safe Pattern)
The array returns Node references, so cast before accessing global_position. For more iteration patterns and safe removal techniques, see arrays & loops.
const GROUP_ENEMY := &"enemy"
var closest: Node2D = null
var best_d2 := INF
for node in get_tree().get_nodes_in_group(GROUP_ENEMY):
var enemy := node as Node2D
if enemy == null:
continue # Skip non-Node2D entries
# Squared distance is cheaper (no sqrt)
var d2 := global_position.distance_squared_to(enemy.global_position)
if d2 < best_d2:
best_d2 = d2
closest = enemy
Performance note: get_nodes_in_group() allocates an Array every call. Don't call it every frame—cache the result or use signals for dynamic membership.
In Godot 4, group names and method names are StringName parameters. Using the &"literal" syntax avoids String→StringName conversion and enables fast comparisons:
# Best practice: define as StringName constants
const GROUP_PLAYER := &"player"
const GROUP_ENEMY := &"enemy"
const GROUP_PICKUP := &"pickup"
# Usage
add_to_group(GROUP_ENEMY)
if body.is_in_group(GROUP_PLAYER):
...
get_tree().call_group(GROUP_ENEMY, &"freeze", 1.0)
This is a small optimization, but it signals "I know the Godot 4 API."
| Pattern | Examples | Notes |
|---|---|---|
| Singular nouns | player, enemy, pickup | Most common, reads naturally |
| Role groups | damageable, collectible, interactable | Good for shared interfaces |
| Avoid state groups | stunnedfrozen | State belongs in variables, not group membership |
State groups seem convenient but create sync bugs—you'll forget to remove the group when state changes. Use a var is_stunned: bool instead. For advanced group-based data patterns like mapping group members to stats, see our dictionary map blog post.
When to Use GDScript Groups & Pickups
- Pickups, coins, and collectibles that should only react to players
- Global actions: pause all AI, despawn all bullets, heal all allies
- Loose coupling between systems (UI finds player via group, weapons damage anything in "enemy" group)
- Filtering collision callbacks without hard-coding node names or paths
Check Your Understanding: GDScript Groups & Pickups
Check Your Understanding: Why use groups instead of node names or paths?
Names/paths are brittle: they break when you refactor scenes. Groups express intent ("this is a player") and scale to multiple instances. They also reduce coupling: collision code doesn't need to know which scene the player is; it just checks the group. Plus, call_group() lets you broadcast actions to all tagged nodes without maintaining an explicit list.
What You'll Practice: GDScript Groups & Pickups
Common GDScript Groups & Pickups Pitfalls
- Calling get_nodes_in_group() in _process/_physics_process every frame—allocates arrays repeatedly; cache instead
- Assuming call_group() is deferred (Godot 3 behavior)—in Godot 4 it runs immediately; use GROUP_CALL_DEFERRED flag if needed
- Using groups for game state (stunned, frozen)—state belongs in variables; groups create sync bugs when you forget to remove them
- Accessing global_position on uncast Node from get_nodes_in_group()—cast to Node2D first or you'll get errors
- Hard-coding string literals everywhere—use StringName constants (&"enemy") for type safety and performance
GDScript Groups & Pickups FAQ
How do I add a node to a group in the Godot editor?
Select the node → Node tab (next to Inspector) → Groups section → type a name and click Add. For project-wide groups, use Project Settings → Global Groups to define them once, then assign via the Node dock dropdown.
Does call_group() run immediately or at end of frame?
In Godot 4, call_group() acts immediately—not deferred. Nodes without the method are silently ignored. If you need end-of-frame behavior, use call_group_flags(SceneTree.GROUP_CALL_DEFERRED, "group", "method").
What order does get_nodes_in_group() return in Godot 4?
Returns nodes in scene tree (hierarchy) order. However, don't rely on this being stable if nodes are spawned, freed, or reparented during gameplay—sort the array if you need deterministic ordering rules.
How do I get the player node via groups?
get_tree().get_first_node_in_group(&"player") returns the first match (or null). For a single player, this is cleaner than get_nodes_in_group()[0]. Cast the result: var player := get_tree().get_first_node_in_group(&"player") as CharacterBody2D.
Is get_nodes_in_group() expensive?
It allocates a new Array every call. Fine for occasional queries (ability activation, level start), but don't call it every frame in _process. Cache references or use signals to track membership changes.
When should I use groups vs signals?
Groups are for querying and broadcasting to a category of nodes ("affect all enemies"). Signals are for specific events between connected nodes ("this enemy died"). Use both together: groups to find nodes, signals to react to individual events.
How do I safely cast get_nodes_in_group results?
The array contains Node references. Cast each element: for node in get_nodes_in_group(&"enemy"): var enemy := node as Enemy; if enemy == null: continue. This handles cases where non-matching types accidentally joined the group.
What are Global Groups in Godot 4?
Project Settings → Global Groups lets you define groups project-wide. They appear in the Node dock dropdown for easy assignment. Use them to ensure consistent group names across your project.
GDScript Groups & Pickups Syntax Quick Reference
func _ready() -> void:
add_to_group(&"enemy")extends Area2D
@export var heal_amount := 25
func _on_body_entered(body: Node) -> void:
if not body.is_in_group(&"player"):
return
body.call(&"heal", heal_amount)
queue_free()# Runs NOW in Godot 4
get_tree().call_group(&"enemy", &"take_damage", 10)# Runs at end of frame
get_tree().call_group_flags(
SceneTree.GROUP_CALL_DEFERRED,
&"enemy",
&"freeze",
2.0
)var closest: Node2D = null
var best_d2 := INF
for node in get_tree().get_nodes_in_group(&"enemy"):
var e := node as Node2D
if e == null:
continue
var d2 := global_position.distance_squared_to(e.global_position)
if d2 < best_d2:
best_d2 = d2
closest = eGDScript Groups & Pickups Sample Exercises
Fetch the first node in group 'player', storing it in player.
var player = get_tree().get_first_node_in_group("player")Write just the expression (no if keyword) that checks target is still a valid instance.
is_instance_valid(target)Call take_damage(5) on every node in group 'enemies'.
get_tree().call_group("enemies", "take_damage", 5)+ 6 more exercises