第一部分 09:Group 分组系统

2026-05-19
525418 分钟
...

这一章讲 Godot 里的 Group

先用一句话理解:

Group = 给节点打标签然后通过这个标签批量查找判断调用

比如你有很多敌人:

Slime
Bat
Goblin
Wolf

你可以把它们都加入:

enemies

然后代码里就可以:

get_tree().call_group("enemies", "freeze")

意思是:

调用 enemies 组里所有节点的 freeze 方法

Godot 官方文档把 Group 类比成其他软件里的 tag:一个节点可以加入多个 group,然后可以通过 SceneTree 获取某个组里的节点、调用组内所有节点的方法,或者给组内节点发送通知。官方也明确说 Group 很适合组织大型场景和降低代码耦合。(Godot Engine documentation)

1. Group 是什么

Group 可以理解为“节点标签”。

一个节点可以属于一个 group,也可以属于多个 group。

比如一个怪物节点:

Slime CharacterBody2D

可以同时属于:

enemies
damageable
saveable
weather_affected

意思是:

它是敌人
它可以受伤
它需要被存档
它会受天气影响

Group 不改变节点本身的类型。

它只是给节点加一个分类标签。

2. 前端类比

Group 有点像 HTML class。

前端里你可能会写:

<div class="enemy damageable saveable"></div>

然后可以:

document.querySelectorAll(".enemy")

Godot 里类似:

add_to_group("enemies")

然后可以:

get_tree().get_nodes_in_group("enemies")

所以可以粗略类比:

HTML classGodot Group
querySelectorAll(".enemy") ≈ get_nodes_in_group("enemies")

不过 Godot Group 更偏游戏逻辑分类,不只是样式分类。

3. Group 解决什么问题

假设你想让所有敌人暂停。

不用 Group 的写法可能是:

$Enemies/Slime.freeze()
$Enemies/Bat.freeze()
$Enemies/Goblin.freeze()
$Enemies/Wolf.freeze()

这很脆。

因为敌人数量可能变化,路径也可能变化。

用 Group:

get_tree().call_group("enemies", "freeze")

只要敌人加入了 enemies 组,就会收到调用。

这就非常适合:

批量暂停敌人
批量恢复敌人
批量删除子弹
批量隐藏交互提示
批量保存对象数据
批量让对象响应天气
批量让对象进入夜晚状态

4. Group 和 Signal 的区别

这两个很容易混。

Signal 是事件通知

我发生了一件事谁关心谁响应

比如:

Player.health_changedHUD 更新血量
Chest.openedMain 增加金币
WeatherManager.weather_changedWorld 改变颜色

Group 是批量管理

我想找到一批节点或者让一批节点统一执行某个方法

比如:

所有 enemiesfreeze()
所有 saveableget_save_data()
所有 weather_affectedapply_weather("rainy")
所有 interactableshide_prompt()

简单记:

Signal事件广播
Group节点分类和批量操作

再实际一点:

血量变化通知 UI Signal
暂停所有敌人 Group

宝箱打开通知主场景 Signal
查找所有宝箱用于存档 Group

天气变化通知世界Signal 可以
让所有天气影响对象更新状态Group 也可以

5. 怎么把节点加入 Group

有两种方式:

编辑器里添加
代码里添加

Godot 官方文档说明,Group 可以在编辑器的 Groups dock / Project Settings 的 Global Groups 中管理,也可以运行时通过 Node.add_to_group()Node.remove_from_group() 管理。(Godot Engine documentation)

6. 编辑器里添加 Group

操作步骤:

1. 选中一个节点
2. 右侧切到 Node 面板
3. 找到 Groups
4. 添加或勾选 group 名称

比如选中 Slime 节点,添加:

enemies

以后这个 Slime 就属于 enemies 组。

适合编辑器里固定存在的节点,比如:

地图里的 NPC
地图里的宝箱
地图里的门
固定敌人
存档点
天气影响物

7. 代码里添加 Group:add_to_group

最常见写法:

func _ready() -> void:
    add_to_group("enemies")

这表示:

当前节点加入 enemies

比如 Enemy.gd

class_name Enemy
extends CharacterBody2D

func _ready() -> void:
    add_to_group("enemies")

这样所有继承或使用这个脚本的敌人,进入场景后都会自动加入 enemies 组。

官方 Groups 教程也给出了类似写法:在 _ready() 中调用 add_to_group("guards"),然后可以用 get_tree().call_group("guards", "enter_alert_mode") 批量调用组内节点的方法。(Godot Engine documentation)

8. 从 Group 移除:remove_from_group

如果某个节点不再属于某组,可以移除:

remove_from_group("enemies")

比如敌人死亡时:

func die() -> void:
    remove_from_group("enemies")
    queue_free()

不过如果马上 queue_free(),通常不手动移除也没什么问题,因为节点离开场景树后自然不再参与组操作。

更常见的移除场景是:

NPC 临时不可交互
对象暂时不参与天气系统
怪物变成友方不再属于 enemies
物品被拾取后不再属于 collectables

例子:

func disable_interaction() -> void:
    remove_from_group("interactables")

9. 判断节点是否在某个 Group:is_in_group

if body.is_in_group("enemies"):
    print("碰到敌人了")

常用于 Area2D 检测。

比如攻击范围检测敌人:

func _on_attack_area_body_entered(body: Node2D) -> void:
    if body.is_in_group("enemies"):
        body.take_damage(10)

意思是:

进入攻击范围的是 enemies 组成员就让它受伤

这个比判断名字更好。

不推荐:

if body.name == "Slime":
    body.take_damage(10)

因为以后敌人不一定都叫 Slime。

推荐:

if body.is_in_group("enemies"):
    body.take_damage(10)

10. 获取某个 Group 的所有节点:get_nodes_in_group

var enemies = get_tree().get_nodes_in_group("enemies")

这会返回一个数组。

官方文档也写到,可以通过 SceneTree.get_nodes_in_group() 获取某个组内节点列表。(Godot Engine documentation)

例子:

func print_all_enemies() -> void:
    var enemies = get_tree().get_nodes_in_group("enemies")

    for enemy in enemies:
        print(enemy.name)

删除所有敌人:

func clear_all_enemies() -> void:
    var enemies = get_tree().get_nodes_in_group("enemies")

    for enemy in enemies:
        enemy.queue_free()

11. 获取第一个节点:get_first_node_in_group

如果你只需要一个节点,可以用:

var player = get_tree().get_first_node_in_group("player")

SceneTree 文档的方法列表里包含 get_first_node_in_group()get_nodes_in_group()get_node_count_in_group()call_group() 等组相关方法。(Godot Engine documentation)

比如全局查找玩家:

func get_player() -> Player:
    return get_tree().get_first_node_in_group("player") as Player

然后 Player 自己加入组:

class_name Player
extends CharacterBody2D

func _ready() -> void:
    add_to_group("player")

这样别的节点不需要知道 Player 在场景树里的路径。

12. 统计组内节点数量:get_node_count_in_group

var enemy_count = get_tree().get_node_count_in_group("enemies")
print("当前敌人数量:", enemy_count)

适合:

判断战斗是否结束
判断区域内怪物是否清空
调试当前对象数量
限制生成器最大生成数量

比如怪物生成器:

@export var max_enemies: int = 10

func can_spawn_enemy() -> bool:
    return get_tree().get_node_count_in_group("enemies") < max_enemies

13. 批量调用方法:call_group

这是 Group 最常用、最爽的功能之一。

get_tree().call_group("enemies", "freeze")

意思是:

调用 enemies 组里所有节点的 freeze 方法

SceneTree.call_group() 会调用指定 group 中每个节点的某个方法,并且可以在方法名后继续传参数;如果某些节点没有这个方法或参数不匹配,会被忽略。(Godot Engine documentation)

敌人脚本

class_name Enemy
extends CharacterBody2D

var frozen: bool = false

func _ready() -> void:
    add_to_group("enemies")

func freeze() -> void:
    frozen = true
    velocity = Vector2.ZERO

func unfreeze() -> void:
    frozen = false

func _physics_process(delta: float) -> void:
    if frozen:
        return

    # 敌人正常移动逻辑

GameManager 调用

func pause_enemies() -> void:
    get_tree().call_group("enemies", "freeze")

func resume_enemies() -> void:
    get_tree().call_group("enemies", "unfreeze")

这就比一个个找敌人舒服多了。

14. call_group 传参数

假设所有天气影响对象都有:

func apply_weather(weather: String) -> void:
    pass

那么你可以:

get_tree().call_group("weather_affected", "apply_weather", "rainy")

完整例子:

class_name WeatherManager
extends Node

func set_weather(weather: String) -> void:
    get_tree().call_group("weather_affected", "apply_weather", weather)

树节点:

extends Sprite2D

func _ready() -> void:
    add_to_group("weather_affected")

func apply_weather(weather: String) -> void:
    match weather:
        "sunny":
            modulate = Color(1, 1, 1)
        "rainy":
            modulate = Color(0.7, 0.8, 0.9)
        "snowy":
            modulate = Color(0.9, 0.95, 1)

路灯节点:

extends Node2D

@onready var light: PointLight2D = $PointLight2D

func _ready() -> void:
    add_to_group("weather_affected")

func apply_weather(weather: String) -> void:
    if weather == "foggy":
        light.energy = 1.2
    else:
        light.energy = 0.8

这里非常灵活:

WeatherManager 不需要知道有哪些对象会受天气影响
它只要通知 weather_affected
具体对象自己决定怎么响应天气

15. 批量设置属性:set_group

除了调用方法,也可以批量设置属性。

比如隐藏所有调试节点:

get_tree().set_group("debug", "visible", false)

显示:

get_tree().set_group("debug", "visible", true)

SceneTree 的方法列表中包含 set_group()set_group_flags(),它们用于给指定 group 内的节点设置属性。(Godot Engine documentation)

不过我个人建议:

简单属性可以用 set_group
复杂逻辑更推荐 call_group 调方法

比如:

get_tree().set_group("debug", "visible", false)

可以。

但天气变化不要写:

get_tree().set_group("weather_affected", "modulate", Color(0.7, 0.8, 0.9))

因为不同对象可能有不同响应。

更推荐:

get_tree().call_group("weather_affected", "apply_weather", "rainy")

让每个对象自己处理。

16. Group 命名建议

推荐用小写英文 + 下划线。

常见:

player
enemies
npcs
interactables
collectables
damageable
saveable
weather_affected
time_affected
pauseable
debug
doors
chests
projectiles

不太推荐:

Enemies
Enemy Group
可交互对象
所有敌人

不是不能用中文,但后期代码、插件、协作、搜索都不如英文稳定。

推荐语义:

enemies敌人
npcsNPC
interactables可交互对象
damageable可受伤对象
saveable需要存档对象
weather_affected受天气影响对象
pauseable可被暂停对象

17. 一个节点可以加入多个 Group

比如宝箱:

func _ready() -> void:
    add_to_group("interactables")
    add_to_group("saveable")
    add_to_group("chests")

意思是:

它可以交互
它需要存档
它是宝箱

NPC:

func _ready() -> void:
    add_to_group("interactables")
    add_to_group("npcs")
    add_to_group("saveable")

敌人:

func _ready() -> void:
    add_to_group("enemies")
    add_to_group("damageable")
    add_to_group("pauseable")

玩家:

func _ready() -> void:
    add_to_group("player")
    add_to_group("damageable")

这样你的系统就能从不同角度管理同一个节点。

18. Group 适合做“能力标签”

Group 不一定只表示类型,也可以表示能力。

比如:

enemies类型标签表示敌人
damageable能力标签表示可以受伤
interactables能力标签表示可以交互
saveable能力标签表示可以存档
weather_affected能力标签表示受天气影响

这很重要。

因为有些对象不是敌人,但也能受伤:

玩家可以受伤
敌人可以受伤
可破坏箱子可以受伤
训练木桩可以受伤

所以攻击逻辑可以检测:

if body.is_in_group("damageable"):
    body.take_damage(10)

而不是只检测:

if body.is_in_group("enemies"):
    body.take_damage(10)

这样更通用。

19. 攻击系统例子:damageable

玩家攻击区域:

Player CharacterBody2D
└── AttackArea Area2D
    └── CollisionShape2D

敌人脚本:

class_name Enemy
extends CharacterBody2D

@export var max_health: int = 30

var health: int

func _ready() -> void:
    health = max_health
    add_to_group("enemies")
    add_to_group("damageable")

func take_damage(amount: int) -> void:
    health -= amount

    if health <= 0:
        queue_free()

可破坏箱子:

class_name BreakableBox
extends StaticBody2D

@export var max_health: int = 10

var health: int

func _ready() -> void:
    health = max_health
    add_to_group("damageable")

func take_damage(amount: int) -> void:
    health -= amount

    if health <= 0:
        queue_free()

攻击检测:

func _on_attack_area_body_entered(body: Node2D) -> void:
    if not body.is_in_group("damageable"):
        return

    if body.has_method("take_damage"):
        body.take_damage(10)

这样攻击既可以打敌人,也可以打可破坏箱子。

20. 交互系统例子:interactables

可交互对象都加入:

interactables

玩家检测时:

var current_interactable: Node = null

func _on_interact_area_body_entered(body: Node2D) -> void:
    if body.is_in_group("interactables"):
        current_interactable = body

func _on_interact_area_body_exited(body: Node2D) -> void:
    if body == current_interactable:
        current_interactable = null

玩家按 E:

func try_interact() -> void:
    if current_interactable == null:
        return

    if current_interactable.has_method("interact"):
        current_interactable.interact()

宝箱:

class_name Chest
extends StaticBody2D

func _ready() -> void:
    add_to_group("interactables")

func interact() -> void:
    print("打开宝箱")

NPC:

class_name NPC
extends CharacterBody2D

func _ready() -> void:
    add_to_group("interactables")

func interact() -> void:
    print("开始对话")

门:

class_name Door
extends Area2D

func _ready() -> void:
    add_to_group("interactables")

func interact() -> void:
    print("进入房间")

玩家不需要关心对方具体是什么。

只要它:

 interactables
 interact 方法

就能交互。

21. 存档系统例子:saveable

需要存档的对象加入:

saveable

比如宝箱需要记录是否打开过:

class_name Chest
extends StaticBody2D

@export var chest_id: String = "chest_001"

var is_opened: bool = false

func _ready() -> void:
    add_to_group("saveable")
    add_to_group("interactables")

func interact() -> void:
    if is_opened:
        return

    is_opened = true
    print("打开宝箱")

func get_save_data() -> Dictionary:
    return {
        "id": chest_id,
        "is_opened": is_opened
    }

func load_save_data(data: Dictionary) -> void:
    is_opened = data.get("is_opened", false)

SaveManager:

class_name SaveManager
extends Node

func collect_save_data() -> Array[Dictionary]:
    var result: Array[Dictionary] = []

    for node in get_tree().get_nodes_in_group("saveable"):
        if node.has_method("get_save_data"):
            result.append(node.get_save_data())

    return result

这样 SaveManager 不需要知道场景里有哪些宝箱、门、NPC 状态对象。

它只关心:

谁是 saveable
谁有 get_save_data 方法

这就很适合 RPG。

22. 天气系统例子:weather_affected

受天气影响的对象加入:

weather_affected

WeatherManager:

class_name WeatherManager
extends Node

var current_weather: String = "sunny"

func set_weather(weather: String) -> void:
    current_weather = weather
    get_tree().call_group("weather_affected", "apply_weather", weather)

草地装饰:

extends Sprite2D

func _ready() -> void:
    add_to_group("weather_affected")

func apply_weather(weather: String) -> void:
    match weather:
        "sunny":
            modulate = Color(1, 1, 1)
        "rainy":
            modulate = Color(0.75, 0.85, 0.95)
        "snowy":
            modulate = Color(0.9, 0.95, 1)

室内灯:

extends PointLight2D

func _ready() -> void:
    add_to_group("weather_affected")

func apply_weather(weather: String) -> void:
    if weather == "rainy" or weather == "foggy":
        energy = 1.2
    else:
        energy = 0.8

这种方式很适合你后面做:

雨天
雪天
雾天
夜晚
季节变化

23. 暂停系统例子:pauseable

很多对象需要暂停:

敌人
NPC
子弹
天气粒子
移动平台

这些节点加入:

pauseable

敌人:

class_name Enemy
extends CharacterBody2D

var paused_by_game: bool = false

func _ready() -> void:
    add_to_group("pauseable")
    add_to_group("enemies")

func pause_gameplay() -> void:
    paused_by_game = true
    velocity = Vector2.ZERO

func resume_gameplay() -> void:
    paused_by_game = false

func _physics_process(delta: float) -> void:
    if paused_by_game:
        return

    # 正常 AI

GameManager:

func pause_gameplay() -> void:
    get_tree().call_group("pauseable", "pause_gameplay")

func resume_gameplay() -> void:
    get_tree().call_group("pauseable", "resume_gameplay")

注意,这和 get_tree().paused 是两种思路。

get_tree().paused
引擎层面的暂停会受 process_mode 影响

pauseable group
你自己控制哪些玩法对象暂停UI 可以照常运行

入门阶段,Group 这种手动暂停方式更好理解。

24. 调试系统例子:debug

调试节点加入:

debug

比如调试文字、碰撞辅助显示、自定义坐标显示。

func _ready() -> void:
    add_to_group("debug")

然后统一显示隐藏:

func toggle_debug(visible: bool) -> void:
    get_tree().set_group("debug", "visible", visible)

或者:

func show_debug() -> void:
    get_tree().call_group("debug", "show_debug")

func hide_debug() -> void:
    get_tree().call_group("debug", "hide_debug")

25. Door 门系统例子:doors

所有门加入:

doors

Door.gd:

class_name Door
extends Area2D

@export var door_id: String = "house_001"
@export var target_scene_path: String

func _ready() -> void:
    add_to_group("doors")
    add_to_group("interactables")

func interact() -> void:
    print("切换到:", target_scene_path)

func lock() -> void:
    print("门已锁")

func unlock() -> void:
    print("门已解锁")

锁住所有门:

get_tree().call_group("doors", "lock")

解锁所有门:

get_tree().call_group("doors", "unlock")

某个任务完成后可以批量开门:

func _on_quest_completed(quest_id: String) -> void:
    if quest_id == "village_intro":
        get_tree().call_group("doors", "unlock")

26. Projectile 子弹系统例子:projectiles

所有子弹加入:

projectiles

Projectile.gd:

class_name Projectile
extends Area2D

@export var speed: float = 300.0

var direction: Vector2 = Vector2.RIGHT

func _ready() -> void:
    add_to_group("projectiles")

func _physics_process(delta: float) -> void:
    global_position += direction * speed * delta

func destroy() -> void:
    queue_free()

清空所有子弹:

func clear_projectiles() -> void:
    get_tree().call_group("projectiles", "destroy")

常用于:

切换场景
玩家死亡
进入对话
战斗结束
暂停前清理危险对象

27. Group 和节点路径的关系

不用 Group 时,你可能会写:

@onready var enemies_root: Node2D = $World/Enemies

func freeze_all_enemies() -> void:
    for enemy in enemies_root.get_children():
        enemy.freeze()

这个要求所有敌人都必须挂在 $World/Enemies 下面。

用 Group:

func freeze_all_enemies() -> void:
    get_tree().call_group("enemies", "freeze")

敌人可以在任何地方:

World/Enemies/Slime
World/DungeonRoom/Bat
World/BossRoom/Goblin

只要它们属于 enemies 组,就能被找到。

这让结构更灵活。

28. Group 的优点

不依赖具体节点路径
适合批量操作
适合动态生成节点
适合跨场景查找
适合逻辑分类
能降低模块之间的直接依赖

比如动态生成敌人:

func spawn_enemy(enemy_scene: PackedScene, pos: Vector2) -> void:
    var enemy = enemy_scene.instantiate()
    add_child(enemy)
    enemy.global_position = pos

只要 Enemy 自己在 _ready() 里:

add_to_group("enemies")

生成器就不用额外登记它。

29. Group 的缺点

Group 也不是万能的。

常见问题:

组名是字符串写错不容易立刻发现
call_group 调用的方法不存在时可能被忽略
关系可能变隐形
滥用后项目会像一堆标签乱飞

比如:

get_tree().call_group("enemy", "freeze")

但实际组名是:

enemies

那就不会调用到。

所以建议把常用 group 名称做成常量。

30. 用常量保存 Group 名

可以建一个脚本:

# Groups.gd
class_name Groups

const PLAYER = "player"
const ENEMIES = "enemies"
const NPCS = "npcs"
const INTERACTABLES = "interactables"
const DAMAGEABLE = "damageable"
const SAVEABLE = "saveable"
const WEATHER_AFFECTED = "weather_affected"
const PAUSEABLE = "pauseable"
const DEBUG = "debug"

使用:

add_to_group(Groups.ENEMIES)

判断:

if body.is_in_group(Groups.DAMAGEABLE):
    body.take_damage(10)

调用:

get_tree().call_group(Groups.PAUSEABLE, "pause_gameplay")

这样可以减少拼写错误。

不过注意:如果你用 class_name Groups 注册全局类名,脚本名和类名管理要清楚。小项目里也可以先不抽,等组多起来再抽。

31. Group 和 class_name 怎么选

这俩不是一个东西。

class_name

表示脚本类型。

class_name Enemy
extends CharacterBody2D

可以判断:

if body is Enemy:
    body.take_damage(10)

Group

表示逻辑分类或能力标签。

add_to_group("damageable")

可以判断:

if body.is_in_group("damageable"):
    body.take_damage(10)

怎么选

如果你只关心“它是不是某个具体类型”,用 is

if body is Enemy:
    pass

如果你关心“它有没有某种能力/标签”,用 Group。

if body.is_in_group("damageable"):
    pass

比如攻击系统,推荐用:

if body.is_in_group("damageable"):
    body.take_damage(10)

因为可受伤的不一定只有 Enemy。

32. Group 和 has_method 搭配

Group 只是标签,不保证节点真的有某个方法。

所以稳一点可以写:

if body.is_in_group("damageable") and body.has_method("take_damage"):
    body.take_damage(10)

可交互:

if body.is_in_group("interactables") and body.has_method("interact"):
    body.interact()

存档:

if node.is_in_group("saveable") and node.has_method("get_save_data"):
    var data = node.get_save_data()

这种写法很适合初期项目,容错高。

33. call_group 的注意点

call_group 会尝试调用组内所有节点的方法。

比如:

get_tree().call_group("enemies", "freeze")

如果某个 enemies 节点没有 freeze() 方法,Godot 会忽略它。SceneTree.call_group() 文档说明,不能调用该方法的节点会被忽略,包括方法不存在或参数不匹配的情况。(Godot Engine documentation)

这有好有坏。

好处:

不会因为某个节点没方法就立刻炸掉整个调用

坏处:

你可能以为调用成功了但某些节点根本没响应

所以建议:

加入某个批量调用 group 的节点最好都实现对应方法

比如所有 pauseable 都实现:

func pause_gameplay() -> void:
    pass

func resume_gameplay() -> void:
    pass

34. Group 不等于父子层级

这点很重要。

场景树是结构关系:

Main
├── World
│   └── Enemies
│       ├── Slime
│       └── Bat
└── UI

Group 是逻辑关系:

enemies:
- Slime
- Bat
- Boss
- SummonedMonster

一个节点可以在任何路径下,但只要加入了 enemies 组,它就属于 enemies。

所以:

父子节点解决它放在哪里
Group 解决它属于哪一类

35. Group 不会自动给你方法

如果你写:

add_to_group("interactables")

这只表示节点属于 interactables

它不会自动拥有:

func interact():
    pass

你要自己写。

比如:

func interact() -> void:
    print("被交互")

所以推荐约定:

interactables 组的节点应该有 interact 方法
damageable 组的节点应该有 take_damage 方法
saveable 组的节点应该有 get_save_data / load_save_data 方法
weather_affected 组的节点应该有 apply_weather 方法
pauseable 组的节点应该有 pause_gameplay / resume_gameplay 方法

36. Group 与 Autoload 的配合

后面你可能会有全局管理器,比如:

GameManager
WeatherManager
SaveManager
QuestManager

这些管理器可以通过 Group 找对象。

比如 SaveManager:

class_name SaveManager
extends Node

func save_game() -> void:
    var saveables = get_tree().get_nodes_in_group("saveable")

    for node in saveables:
        if node.has_method("get_save_data"):
            var data = node.get_save_data()
            print(data)

WeatherManager:

class_name WeatherManager
extends Node

func apply_weather(weather: String) -> void:
    get_tree().call_group("weather_affected", "apply_weather", weather)

GameManager:

class_name GameManager
extends Node

func pause_world() -> void:
    get_tree().call_group("pauseable", "pause_gameplay")

这种模式很适合中后期项目。

37. Group 与动态生成敌人

Enemy.gd:

class_name Enemy
extends CharacterBody2D

func _ready() -> void:
    add_to_group("enemies")
    add_to_group("damageable")
    add_to_group("pauseable")

Spawner.gd:

class_name EnemySpawner
extends Node2D

@export var enemy_scene: PackedScene

func spawn_enemy(pos: Vector2) -> void:
    var enemy = enemy_scene.instantiate()
    add_child(enemy)
    enemy.global_position = pos

这里 Spawner 不需要写:

enemy.add_to_group("enemies")

因为 Enemy 自己知道自己是敌人。

这更符合职责分离:

Enemy 负责声明自己属于哪些组
Spawner 只负责生成 Enemy

38. 例子:敌人警戒系统

假设一个守卫发现玩家后,所有敌人进入警戒。

Enemy.gd:

class_name Enemy
extends CharacterBody2D

var alert: bool = false

func _ready() -> void:
    add_to_group("enemies")

func enter_alert_mode() -> void:
    alert = true
    print(name, "进入警戒状态")

GuardVision.gd:

extends Area2D

func _on_body_entered(body: Node2D) -> void:
    if body.is_in_group("player"):
        get_tree().call_group("enemies", "enter_alert_mode")

这和官方 Groups 教程里的 guards 例子思路一样:玩家被发现后,通过 SceneTree.call_group() 调用某个 group 内所有节点的方法。(Godot Engine documentation)

39. 例子:所有 NPC 进入夜晚状态

NPC.gd:

class_name NPC
extends CharacterBody2D

func _ready() -> void:
    add_to_group("npcs")
    add_to_group("time_affected")

func apply_hour(hour: int) -> void:
    if hour >= 20 or hour < 6:
        go_home()
    else:
        start_daily_routine()

func go_home() -> void:
    print(name, "回家")

func start_daily_routine() -> void:
    print(name, "开始白天日程")

TimeManager.gd:

class_name TimeManager
extends Node

var hour: int = 6

func set_hour(value: int) -> void:
    hour = value
    get_tree().call_group("time_affected", "apply_hour", hour)

这样以后你的 NPC 系统就可以扩展:

白天在村里
晚上回家
雨天待在屋里
节日去广场

40. 例子:所有可采集物刷新

采集物:

class_name CollectableResource
extends Area2D

var available: bool = true

func _ready() -> void:
    add_to_group("collectable_resources")

func collect() -> void:
    if not available:
        return

    available = false
    visible = false

func respawn() -> void:
    available = true
    visible = true

TimeManager:

func next_day() -> void:
    get_tree().call_group("collectable_resources", "respawn")

这样每天刷新所有可采集物。

41. 例子:所有室外物体受季节影响

树:

extends Sprite2D

func _ready() -> void:
    add_to_group("season_affected")

func apply_season(season: String) -> void:
    match season:
        "spring":
            texture = preload("res://assets/tree_spring.png")
        "summer":
            texture = preload("res://assets/tree_summer.png")
        "autumn":
            texture = preload("res://assets/tree_autumn.png")
        "winter":
            texture = preload("res://assets/tree_winter.png")

SeasonManager:

func set_season(season: String) -> void:
    get_tree().call_group("season_affected", "apply_season", season)

这就是 Group 在模拟经营、RPG、星露谷类游戏里特别好用的地方。

42. Group 和存档 ID

如果你做存档系统,Group 负责找到 saveable,但每个对象还需要唯一 ID。

@export var save_id: String = "chest_001"

不推荐只靠节点名存档:

name

因为节点名可能改。

更推荐:

@export var save_id: String

SaveManager:

func collect_save_data() -> Dictionary:
    var save_data := {}

    for node in get_tree().get_nodes_in_group("saveable"):
        if node.has_method("get_save_data"):
            var data: Dictionary = node.get_save_data()
            var id: String = data.get("id", "")

            if id != "":
                save_data[id] = data

    return save_data

宝箱:

func get_save_data() -> Dictionary:
    return {
        "id": save_id,
        "is_opened": is_opened
    }

43. Group 与 Area2D 检测

Area2D 最常和 Group 搭配。

比如陷阱只伤害玩家和敌人,不伤害宝箱。

func _on_body_entered(body: Node2D) -> void:
    if body.is_in_group("damageable") and body.has_method("take_damage"):
        body.take_damage(10)

门只检测玩家:

func _on_body_entered(body: Node2D) -> void:
    if body.is_in_group("player"):
        show_enter_hint()

攻击只检测敌人:

func _on_body_entered(body: Node2D) -> void:
    if body.is_in_group("enemies"):
        body.take_damage(attack_damage)

交互范围只检测可交互对象:

func _on_body_entered(body: Node2D) -> void:
    if body.is_in_group("interactables"):
        current_interactable = body

44. Group 和 Collision Layer 的区别

这俩也容易混。

Collision Layer / Mask

用于物理系统:

谁和谁发生碰撞
谁能检测谁

比如:

Player World 碰撞
AttackArea 检测 Enemies
InteractArea 检测 Interactables

Group

用于逻辑系统:

这个节点属于哪类
能不能批量调用
能不能被某段逻辑识别

比如:

body.is_in_group("damageable")
get_tree().call_group("enemies", "freeze")

简单记:

Collision Layer 决定物理上能不能碰到
Group 决定逻辑上怎么看待它

一个对象可以在物理层是 Enemy,也可以在 Group 里是:

enemies
damageable
pauseable
saveable

45. Group 和 Scene 的关系

Group 是运行时节点上的标签。

如果你在一个 Scene 的根节点上设置了 group,这个 Scene 实例化后,实例也会带着这个 group。

比如 slime.tscn 根节点加入:

enemies
damageable

那么你在地图里放 10 个 Slime 实例,它们都会属于这些组。

这就很方便。

建议:

可复用对象的 group尽量配置在它自己的场景里

比如:

Enemy.tscn 自己加入 enemiesdamageable
Chest.tscn 自己加入 interactablessaveable
NPC.tscn 自己加入 npcsinteractables
Door.tscn 自己加入 doorsinteractables

不要把这些 group 全部在 Main 里手动加。

46. Group 使用建议

推荐用 Group 的情况

需要批量操作一类节点
需要跨路径查找节点
动态生成对象很多
对象拥有某种能力标签
存档系统需要找所有对象
天气/时间/季节系统影响很多对象
攻击/交互需要判断对象类别

不推荐用 Group 的情况

只是父节点访问固定子节点
同一个对象内部逻辑
明确知道唯一目标节点
UI 内部按钮响应
简单动画播放

比如 Player 内部拿自己的 Sprite:

@onready var body_sprite: Sprite2D = $BodySprite

不需要 Group。

Player 内部播放动画:

animation_player.play("walk_down")

不需要 Group。

47. 推荐你的 RPG 初期 Group 表

可以先设计这些:

player
玩家

enemies
敌人

npcs
NPC

interactables
可交互对象比如 NPC宝箱采集物

damageable
可受伤对象比如玩家敌人可破坏箱子

saveable
需要存档的对象比如宝箱NPC 状态采集物状态

weather_affected
受天气影响的对象比如草粒子环境音

time_affected
受时间影响的对象比如 NPC路灯商店怪物刷新点

season_affected
受季节影响的对象比如树作物地表装饰

pauseable
玩法暂停时需要停止的对象比如敌人NPC子弹

projectiles
子弹飞行道具

collectables
可拾取物

debug
调试显示节点

48. 一个完整小型 Group 架构例子

场景结构:

Main Node2D
├── Player
├── World Node2D
│   ├── Enemies Node2D
│   │   ├── Slime
│   │   └── Bat
│   ├── Objects Node2D
│   │   ├── Chest
│   │   └── Door
│   └── NPCs Node2D
│       └── Villager
├── Systems Node
│   ├── WeatherManager
│   ├── TimeManager
│   └── SaveManager
└── UI CanvasLayer
    └── HUD

Player.gd:

class_name Player
extends CharacterBody2D

func _ready() -> void:
    add_to_group("player")
    add_to_group("damageable")

Enemy.gd:

class_name Enemy
extends CharacterBody2D

func _ready() -> void:
    add_to_group("enemies")
    add_to_group("damageable")
    add_to_group("pauseable")

func take_damage(amount: int) -> void:
    print("敌人受伤:", amount)

func pause_gameplay() -> void:
    set_physics_process(false)

func resume_gameplay() -> void:
    set_physics_process(true)

Chest.gd:

class_name Chest
extends StaticBody2D

@export var save_id: String = "chest_001"

var is_opened: bool = false

func _ready() -> void:
    add_to_group("interactables")
    add_to_group("saveable")

func interact() -> void:
    if is_opened:
        return

    is_opened = true
    print("打开宝箱")

func get_save_data() -> Dictionary:
    return {
        "id": save_id,
        "is_opened": is_opened
    }

WeatherManager.gd:

class_name WeatherManager
extends Node

func set_weather(weather: String) -> void:
    get_tree().call_group("weather_affected", "apply_weather", weather)

SaveManager.gd:

class_name SaveManager
extends Node

func collect_save_data() -> Array[Dictionary]:
    var result: Array[Dictionary] = []

    for node in get_tree().get_nodes_in_group("saveable"):
        if node.has_method("get_save_data"):
            result.append(node.get_save_data())

    return result

GameManager.gd:

class_name GameManager
extends Node

func pause_gameplay() -> void:
    get_tree().call_group("pauseable", "pause_gameplay")

func resume_gameplay() -> void:
    get_tree().call_group("pauseable", "resume_gameplay")

这个小架构已经很够用了。

49. 常见错误

错误 1:Group 名写错

add_to_group("enemies")

但调用时写:

get_tree().call_group("enemy", "freeze")

enemyenemies 不一样。

建议统一命名,必要时用常量。

错误 2:以为加入 Group 就自动有方法

add_to_group("interactables")

不代表它自动有:

func interact():
    pass

方法要自己写。

错误 3:call_group 的方法名写错

get_tree().call_group("enemies", "freez")

但敌人方法叫:

func freeze():
    pass

这就不会正常调用。

错误 4:把所有东西都塞进 Group

Group 是分类,不是垃圾桶。

不要随手给所有节点加一堆不清楚的 group。

推荐:

只有当你真的需要查找判断或批量调用时再加 group

错误 5:Group 代替了清晰引用

比如 UI 内部按钮:

HUD
└── StartButton

直接:

@onready var start_button: Button = $StartButton

就很好。

没必要:

get_tree().get_first_node_in_group("start_button")

固定父子关系就用节点路径。

跨结构、批量对象、动态对象,再用 Group。

50. Group 使用速查

# 当前节点加入组
add_to_group("enemies")

# 当前节点离开组
remove_from_group("enemies")

# 判断节点是否在组里
if body.is_in_group("enemies"):
    pass

# 获取组内所有节点
var enemies = get_tree().get_nodes_in_group("enemies")

# 获取组内第一个节点
var player = get_tree().get_first_node_in_group("player")

# 获取组内节点数量
var count = get_tree().get_node_count_in_group("enemies")

# 调用组内所有节点的方法
get_tree().call_group("enemies", "freeze")

# 调用组内所有节点的方法并传参数
get_tree().call_group("weather_affected", "apply_weather", "rainy")

# 设置组内所有节点的属性
get_tree().set_group("debug", "visible", false)

51. 这一章你先记住这些

Group 是节点标签
一个节点可以加入多个 Group
add_to_group 用来加入组
remove_from_group 用来移出组
is_in_group 用来判断节点是否属于某组
get_nodes_in_group 用来获取组内所有节点
call_group 用来批量调用组内节点的方法
set_group 可以批量设置组内节点属性
Group 很适合敌人可交互物可受伤对象存档对象天气影响对象
Signal 是事件通知Group 是批量分类管理

52. 最重要的一句话

Group 让你不用关心节点在哪里只关心它属于哪一类

再压缩一下:

找所有敌人enemies
判断能不能受伤damageable
判断能不能交互interactables
收集存档对象saveable
应用天气变化weather_affected
暂停玩法对象pauseable

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
godot2026-07-27
Godot 4 常用 UI 节点详解
Godot 4 常用 UI 节点详解 在 Godot 4 中,UI 系统基于 Control(控件) 节点构建。所有 UI 节点都继承自 Control,形成一棵完整的 UI 树。与游戏引擎中常见的"Canvas + DOM"模式不同,Godot 的 UI 系统是声明式的——你在场景中搭好节点树,引擎自动完成布局计算。 一、布局系统:Container 家族 Container 是 Godot UI 的 骨架 。它决定了子节点的大小和位...
学习
godot2026-07-09
Godot 4 自动地形系统(AutoTileSet / Terrains)完全指南
前言 在 2D 游戏开发中,地形瓦片(tile)的拼接是一个绕不开的问题。想象一下:你有一片草地、一条河流、一段平台——如果每一块边缘、角落、过渡区域都要手动选择对应的瓦片图,工作量将是巨大的。 自动地形系统 就是为了解决这个问题而生的。 一、什么是自动地形(Autotiling)? 自动地形的核心思想很简单: 你只管画,引擎帮你选对瓦片 。 当你在 TileMap 上绘制地形时,引擎会自动检测每个瓦片的上下左右邻居,然后根据预设的规则...
学习
godot2026-06-25
Godot 中 zindex 和 ysort 的区别总结
在 Godot 2D 游戏开发中,角色、树木、怪物、地面、技能特效、UI 都需要正确的显示顺序。比如角色走到树前面时,角色应该挡住树;角色走到树后面时,树又应该挡住角色。 这种显示顺序主要和两个概念有关: 和 。 其中 用来手动控制图层顺序, 用来根据物体的 Y 坐标自动排序。 一句话理解 是手动分层。 是根据 Y 坐标自动排序。 简单来说: | 属性 | 作用 | 适合场景 | | | | | | | 数值越大,显示越靠前 | 地面、...
学习
godot2026-06-11
Tileset 资源图的标准和规范
一、什么是 Tileset 资源图 Tileset,中文通常叫“图块资源图”或“瓦片图”,是 2D 游戏中非常常见的一种地图资源组织方式。 简单来说,Tileset 就是把很多小图块按照固定尺寸排列在一张图片里。游戏引擎会按照固定的格子大小去切割这张图片,然后把每个小格子当成一个独立的地图块使用。 比如一个 32×32 像素的 Tileset 中,每一个 tile 都是 32×32 像素。地图编辑器或游戏引擎会按照 32×32 的网格,...
学习
godot2026-05-29
用户角色精灵图制作角色的完整流程
在 2D RPG 游戏里,角色通常不是用一张单独图片完成的,而是用一张“角色精灵图”来做。 所谓角色精灵图,通常是一张包含多个动作帧的大图。比如角色向下走有 4 帧,向左走有 4 帧,向右走有 4 帧,向上走有 4 帧。Godot 会根据这些帧不断切换图片,看起来角色就动起来了。 这篇文章主要介绍:拿到一张角色精灵图之后,如何在 Godot 中把它做成一个可以正常移动、播放动画、和地图产生遮挡关系的角色。 一、先理解角色精灵图是什么 角...
学习
godot2026-05-26
Godot 节点系统详细介绍
Godot 里最核心的东西不是“类”,也不是“组件”,而是 节点 Node 。 你可以把 Godot 的节点理解成: 在前端里,一个页面是由很多 DOM 元素组成的; 在 Godot 里,一个游戏场景是由很多 Node 节点组成的。 比如一个玩家角色,可能不是一个单独对象,而是这样的结构: 这里的 是根节点,下面挂着显示图片、播放动画、碰撞检测、摄像机、音效等子节点。 Godot 官方文档也把节点和场景放在一起讲:多个节点组成树状结构后...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录