Node.remove_child
2026/4/14大约 6 分钟
最后同步日期:2026-04-16 | Godot 官方原文 — Node.remove_child
Node.remove_child
定义
remove_child 就像从一个文件夹里拿出一个文件——你把一个子节点从它的父节点下面"摘"下来。摘下来之后,这个节点就从场景树中消失了,但它并没有被删除,只是"脱离了组织"。
打个比方:你有一本书放在书架上(子节点挂在父节点下),remove_child 就是把书从书架上拿下来放到桌上。书还在,只是不在书架上了。你可以把它放到另一个书架上(用 add_child 挂到别的父节点下),也可以把它扔掉(用 Free() / free() 彻底销毁)。
这是 add_child 的反向操作——一个负责"挂上去",一个负责"摘下来"。
函数签名
C#
public void RemoveChild(Node node)GDScript
remove_child(node: Node) -> void参数说明
| 参数 | 类型 | 必需 | 说明 |
|---|---|---|---|
| node | Node | 是 | 要移除的子节点。必须是当前节点的直接子节点,否则会报错 |
返回值
无返回值(void)。
代码示例
基础用法
最简单的用法——先添加一个子节点,再把它移除:
C#
public override void _Ready()
{
// 创建并添加一个子节点
var child = new Node();
child.Name = "MyChild";
AddChild(child);
GD.Print(GetChildCount()); // 运行结果: 1
// 把子节点移除
RemoveChild(child);
GD.Print(GetChildCount()); // 运行结果: 0
// 节点还在内存中,只是脱离了场景树
GD.Print(child.Name); // 运行结果: MyChild
GD.Print(child.IsInsideTree()); // 运行结果: False
}GDScript
func _ready():
# 创建并添加一个子节点
var child = Node.new()
child.name = "MyChild"
add_child(child)
print(get_child_count()) # 运行结果: 1
# 把子节点移除
remove_child(child)
print(get_child_count()) # 运行结果: 0
# 节点还在内存中,只是脱离了场景树
print(child.name) # 运行结果: MyChild
print(child.is_inside_tree()) # 运行结果: False实际场景
在游戏中"转移"一个道具:把剑从一个背包容器移到另一个背包容器。这个例子展示了 remove_child 和 add_child 配合使用的典型模式——从一个父节点摘下来,挂到另一个父节点下面:
C#
public partial class InventoryManager : Node
{
private Node _playerBackpack;
private Node _chestStorage;
public override void _Ready()
{
// 初始化两个容器
_playerBackpack = GetNode<Node>("PlayerBackpack");
_chestStorage = GetNode<Node>("ChestStorage");
// 创建一把剑,放入玩家背包
var sword = new Node();
sword.Name = "IronSword";
_playerBackpack.AddChild(sword);
GD.Print("转移前:");
GD.Print($" 玩家背包: {_playerBackpack.GetChildCount()} 件物品"); // 运行结果: 玩家背包: 1 件物品
GD.Print($" 宝箱: {_chestStorage.GetChildCount()} 件物品"); // 运行结果: 宝箱: 0 件物品
// 把剑从玩家背包移到宝箱
_playerBackpack.RemoveChild(sword);
_chestStorage.AddChild(sword);
GD.Print("转移后:");
GD.Print($" 玩家背包: {_playerBackpack.GetChildCount()} 件物品"); // 运行结果: 玩家背包: 0 件物品
GD.Print($" 宝箱: {_chestStorage.GetChildCount()} 件物品"); // 运行结果: 宝箱: 1 件物品
GD.Print($" 剑的新路径: {sword.GetPath()}"); // 运行结果: 剑的新路径: /root/Scene/ChestStorage/IronSword
}
}GDScript
extends Node
var player_backpack: Node
var chest_storage: Node
func _ready():
# 初始化两个容器
player_backpack = get_node("PlayerBackpack")
chest_storage = get_node("ChestStorage")
# 创建一把剑,放入玩家背包
var sword = Node.new()
sword.name = "IronSword"
player_backpack.add_child(sword)
print("转移前:")
print(" 玩家背包: %d 件物品" % player_backpack.get_child_count()) # 运行结果: 玩家背包: 1 件物品
print(" 宝箱: %d 件物品" % chest_storage.get_child_count()) # 运行结果: 宝箱: 0 件物品
# 把剑从玩家背包移到宝箱
player_backpack.remove_child(sword)
chest_storage.add_child(sword)
print("转移后:")
print(" 玩家背包: %d 件物品" % player_backpack.get_child_count()) # 运行结果: 玩家背包: 0 件物品
print(" 宝箱: %d 件物品" % chest_storage.get_child_count()) # 运行结果: 宝箱: 1 件物品
print(" 剑的新路径: %s" % sword.get_path()) # 运行结果: 剑的新路径: /root/Scene/ChestStorage/IronSword进阶用法
移除子节点后彻底销毁 vs 保留复用,同时演示 _ExitTree 回调的触发时机:
C#
public partial class NodePoolDemo : Node
{
[Export] public int ExPoolSize = 3;
private Node _activeContainer;
private Node _poolContainer;
private int _spawnCount = 0;
public override void _Ready()
{
_activeContainer = GetNode<Node>("ActiveContainer");
_poolContainer = GetNode<Node>("PoolContainer");
// 预创建 3 个子弹放入对象池
for (int i = 0; i < ExPoolSize; i++)
{
var bullet = new Node();
bullet.Name = $"Bullet_{i}";
_poolContainer.AddChild(bullet);
}
GD.Print("--- 初始状态 ---");
PrintStatus();
// 运行结果:
// 活跃区: 0 个, 对象池: 3 个
// 从对象池取出一个子弹使用
var firstBullet = _poolContainer.GetChild(0);
_poolContainer.RemoveChild(firstBullet);
_activeContainer.AddChild(firstBullet);
_spawnCount++;
GD.Print($"--- 发射第 {_spawnCount} 颗子弹 ---");
PrintStatus();
// 运行结果:
// 活跃区: 1 个, 对象池: 2 个
// 子弹"用完"了,从活跃区移除并放回对象池(复用,不销毁)
_activeContainer.RemoveChild(firstBullet);
_poolContainer.AddChild(firstBullet);
GD.Print("--- 子弹回收到对象池 ---");
PrintStatus();
// 运行结果:
// 活跃区: 0 个, 对象池: 3 个
// 复用节省了反复创建和销毁的开销
// ---- 对比:如果不需要复用,可以直接销毁 ----
var tempNode = new Node();
tempNode.Name = "TempNode";
AddChild(tempNode);
RemoveChild(tempNode); // 从场景树摘下来
tempNode.Free(); // 彻底销毁,释放内存
// 运行结果: tempNode 已被销毁,之后不能再访问它
}
private void PrintStatus()
{
GD.Print($"活跃区: {_activeContainer.GetChildCount()} 个, 对象池: {_poolContainer.GetChildCount()} 个");
}
}GDScript
extends Node
@export var ex_pool_size: int = 3
var active_container: Node
var pool_container: Node
var _spawn_count: int = 0
func _ready():
active_container = get_node("ActiveContainer")
pool_container = get_node("PoolContainer")
# 预创建 3 个子弹放入对象池
for i in range(ex_pool_size):
var bullet = Node.new()
bullet.name = "Bullet_%d" % i
pool_container.add_child(bullet)
print("--- 初始状态 ---")
print_status()
# 运行结果:
# 活跃区: 0 个, 对象池: 3 个
# 从对象池取出一个子弹使用
var first_bullet = pool_container.get_child(0)
pool_container.remove_child(first_bullet)
active_container.add_child(first_bullet)
_spawn_count += 1
print("--- 发射第 %d 颗子弹 ---" % _spawn_count)
print_status()
# 运行结果:
# 活跃区: 1 个, 对象池: 2 个
# 子弹"用完"了,从活跃区移除并放回对象池(复用,不销毁)
active_container.remove_child(first_bullet)
pool_container.add_child(first_bullet)
print("--- 子弹回收到对象池 ---")
print_status()
# 运行结果:
# 活跃区: 0 个, 对象池: 3 个
# 复用节省了反复创建和销毁的开销
# ---- 对比:如果不需要复用,可以直接销毁 ----
var temp_node = Node.new()
temp_node.name = "TempNode"
add_child(temp_node)
remove_child(temp_node) # 从场景树摘下来
temp_node.free() # 彻底销毁,释放内存
# 运行结果: temp_node 已被销毁,之后不能再访问它
func print_status():
print("活跃区: %d 个, 对象池: %d 个" % [active_container.get_child_count(), pool_container.get_child_count()])注意事项
- 移除 ≠ 删除:
remove_child只是把节点从场景树中"摘"下来,节点仍然存在于内存中。如果你不再需要这个节点,必须手动调用Free()(C#)或free()(GDScript)来释放内存,否则会造成内存泄漏。 - 移除后会触发
_ExitTree回调:节点被移除后,它的_ExitTree()方法会被调用,同时它的所有子节点的_ExitTree()也会被递归调用。你可以在这个回调中做清理工作,比如停止音效、断开信号连接等。 - 移除后节点路径失效:被移除的节点不再有有效的场景树路径(
GetPath()会报错),直到它被重新add_child到某个节点下面。 - 只能移除自己的直接子节点:你不能调用
RemoveChild去移除别人的子节点。如果需要操作孙节点,要先获取到它的父节点,再由父节点执行移除。例如:grandChild.GetParent().RemoveChild(grandChild)。 - 移除后可以重新添加:被移除的节点可以挂到任何其他节点下面,这是实现"节点转移"和"对象池"模式的基础。
- 与
QueueFree的区别:QueueFree是"移除 + 销毁"(在当前帧结束后),而remove_child只是"移除"。如果你只需要临时隐藏节点并稍后复用,用remove_child;如果确定再也不需要了,直接用QueueFree更方便。 - C# 差异:C# 中方法名用 PascalCase(
RemoveChild),GDScript 中用 snake_case(remove_child)。C# 中销毁节点用Free(),GDScript 中用free()。
