第一部分 09:Group 分组系统
这一章讲 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 class ≈ Godot 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_changed → HUD 更新血量
Chest.opened → Main 增加金币
WeatherManager.weather_changed → World 改变颜色
Group 是批量管理
我想找到一批节点,或者让一批节点统一执行某个方法。
比如:
所有 enemies → freeze()
所有 saveable → get_save_data()
所有 weather_affected → apply_weather("rainy")
所有 interactables → hide_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:敌人
npcs:NPC
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 自己加入 enemies、damageable
Chest.tscn 自己加入 interactables、saveable
NPC.tscn 自己加入 npcs、interactables
Door.tscn 自己加入 doors、interactables
不要把这些 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")
enemy 和 enemies 不一样。
建议统一命名,必要时用常量。
错误 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如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录