Node.is_inside_tree
最后同步日期:2026-04-16 | Godot 官方原文 — Node.is_inside_tree
Node.is_inside_tree
定义
is_inside_tree 就像检查一个员工是不是"在职"——它告诉你这个节点当前有没有被"挂"到场景树(SceneTree)上。
打个比方:想象场景树是一家公司的组织架构图,每个节点就是一个员工。一个节点被 add_child 挂上去,就像员工入职了,此时 is_inside_tree 返回 true;一个节点被 remove_child 摘下来或被 queue_free 销毁了,就像员工离职了,此时 is_inside_tree 返回 false。
为什么这个属性很重要?因为很多 Godot 功能只有在节点"在职"时才能使用——比如获取节点路径(GetPath())、访问场景树(GetTree())、使用 _Process 循环等。如果节点不在树中就调用这些功能,程序会报错崩溃。is_inside_tree 就是你的"入职检查器",帮你在调用之前确认节点是否"在职",避免出错。
函数签名
// C# 中是属性(Property),直接读取
public bool IsInsideTree { get; }# GDScript 中是方法(Method),需要加括号调用
is_inside_tree() -> bool参数说明
| 参数 | 类型 | 必需 | 说明 |
|---|---|---|---|
| — | — | — | 无参数。这是一个只读属性(C#)/ 无参方法(GDScript),直接调用即可 |
C# 属性 vs GDScript 方法
C# 中 IsInsideTree 是一个属性,使用时不需要加括号:node.IsInsideTree。
GDScript 中 is_inside_tree() 是一个方法,使用时必须加括号:node.is_inside_tree()。
虽然形式不同,但功能完全一样——都是告诉你节点当前是否在场景树中。
返回值
| 返回值 | 类型 | 说明 |
|---|---|---|
| 结果 | bool | true = 节点当前在场景树中(已"入职");false = 节点不在场景树中(已"离职"或从未加入) |
返回值的变化时机
| 操作 | 返回值变化 |
|---|---|
刚创建节点(new Node()) | false(还没挂到树上) |
调用 add_child 之后 | true(挂上去了) |
调用 remove_child 之后 | false(被摘下来了) |
调用 queue_free 之后(当前帧内) | true(要等到帧结束才真正销毁) |
调用 queue_free 之后(下一帧) | 节点已被销毁,无法访问 |
_EnterTree 回调中 | true(刚刚进入场景树) |
_ExitTree 回调中 | false(正在离开场景树) |
代码示例
基础用法
最简单的用法——观察一个节点从"创建"到"挂载"再到"摘除"的过程中,is_inside_tree 返回值的变化:
public override void _Ready()
{
// ① 刚创建的节点,还没挂到场景树
var child = new Node();
child.Name = "TestChild";
GD.Print(child.IsInsideTree); // 运行结果: False
// ② 调用 add_child 后,节点进入场景树
AddChild(child);
GD.Print(child.IsInsideTree); // 运行结果: True
// ③ 调用 remove_child 后,节点离开场景树
RemoveChild(child);
GD.Print(child.IsInsideTree); // 运行结果: False
// ④ 重新挂载,又回到场景树
AddChild(child);
GD.Print(child.IsInsideTree); // 运行结果: True
// 用完记得清理
child.QueueFree();
}func _ready():
# ① 刚创建的节点,还没挂到场景树
var child = Node.new()
child.name = "TestChild"
print(child.is_inside_tree()) # 运行结果: False
# ② 调用 add_child 后,节点进入场景树
add_child(child)
print(child.is_inside_tree()) # 运行结果: True
# ③ 调用 remove_child 后,节点离开场景树
remove_child(child)
print(child.is_inside_tree()) # 运行结果: False
# ④ 重新挂载,又回到场景树
add_child(child)
print(child.is_inside_tree()) # 运行结果: True
# 用完记得清理
child.queue_free()实际场景
在游戏中,很多操作需要节点在场景树中才能执行。比如你想访问 GetTree() 来暂停游戏、或者用 GetPath() 获取节点路径——如果节点不在树中,这些操作会报错。is_inside_tree 就是你的"安全检查",先确认节点在职,再执行操作:
public partial class SafeNodeAccess : Node
{
private Node _weaponNode;
public override void _Ready()
{
// 创建一个武器节点并挂载
_weaponNode = new Node();
_weaponNode.Name = "Weapon";
AddChild(_weaponNode);
// 安全地执行需要"在职"才能做的操作
TryAccessTreeFeatures(_weaponNode);
// 运行结果:
// 节点 Weapon 在场景树中: True
// 节点路径: /root/SafeNodeAccess/Weapon
// 把武器从场景树摘下来(比如装备到其他角色身上)
RemoveChild(_weaponNode);
// 再次尝试——这次节点已经不在树中了
TryAccessTreeFeatures(_weaponNode);
// 运行结果:
// 节点 Weapon 在场景树中: False
// 节点不在场景树中,跳过需要场景树的操作
}
/// <summary>
/// 安全地访问需要节点在场景树中的功能。
/// 先用 IsInsideTree 做检查,避免报错。
/// </summary>
private void TryAccessTreeFeatures(Node node)
{
GD.Print($"节点 {node.Name} 在场景树中: {node.IsInsideTree}");
if (node.IsInsideTree)
{
// 这些操作只有节点在场景树中才能执行
GD.Print($"节点路径: {node.GetPath()}");
// 也可以访问 GetTree()、ProcessMode 等
}
else
{
// 节点不在场景树中,走降级逻辑
GD.Print("节点不在场景树中,跳过需要场景树的操作");
}
}
}extends Node
var _weapon_node: Node
func _ready():
# 创建一个武器节点并挂载
_weapon_node = Node.new()
_weapon_node.name = "Weapon"
add_child(_weapon_node)
# 安全地执行需要"在职"才能做的操作
try_access_tree_features(_weapon_node)
# 运行结果:
# 节点 Weapon 在场景树中: True
# 节点路径: /root/SafeNodeAccess/Weapon
# 把武器从场景树摘下来(比如装备到其他角色身上)
remove_child(_weapon_node)
# 再次尝试——这次节点已经不在树中了
try_access_tree_features(_weapon_node)
# 运行结果:
# 节点 Weapon 在场景树中: False
# 节点不在场景树中,跳过需要场景树的操作
# 安全地访问需要节点在场景树中的功能。
# 先用 is_inside_tree() 做检查,避免报错。
func try_access_tree_features(node: Node):
print("节点 %s 在场景树中: %s" % [node.name, node.is_inside_tree()])
if node.is_inside_tree():
# 这些操作只有节点在场景树中才能执行
print("节点路径: %s" % node.get_path())
# 也可以访问 get_tree()、process_mode 等
else:
# 节点不在场景树中,走降级逻辑
print("节点不在场景树中,跳过需要场景树的操作")进阶用法
在定时器回调、信号处理等"延迟执行"的场景中,节点可能在回调触发时已经被移除或销毁了。这时候用 is_inside_tree 做防御性检查是非常实用的编程模式。本例演示了定时器回调中的安全检查、queue_free 后的状态变化,以及批量操作中的节点筛选:
public partial class DeferredActionDemo : Node
{
[Export] public float ExDeleteDelay = 2.0f;
private Node _itemA;
private Node _itemB;
private Node _itemC;
public override void _Ready()
{
// 创建 3 个道具节点
_itemA = new Node(); _itemA.Name = "ItemA";
_itemB = new Node(); _itemB.Name = "ItemB";
_itemC = new Node(); _itemC.Name = "ItemC";
AddChild(_itemA);
AddChild(_itemB);
AddChild(_itemC);
// 场景 1:定时器回调中做安全检查
// 2 秒后执行回调,但中途可能已经把节点移除了
GetTree().CreateTimer(ExDeleteDelay).Timeout += () =>
{
// 定时器触发时,先检查节点是否还在场景树中
if (_itemA != null && _itemA.IsInsideTree)
{
GD.Print($"定时器触发: {_itemA.Name} 还在,执行操作");
// 运行结果: 如果没被移除,会走到这里
}
else
{
GD.Print("定时器触发: ItemA 已经不在场景树了,跳过");
// 运行结果: 如果中途被移除了,走这个分支
}
};
// 提前移除 ItemA,模拟"道具被拾走"
RemoveChild(_itemA);
GD.Print($"ItemA 被移除后: IsInsideTree = {_itemA.IsInsideTree}");
// 运行结果: ItemA 被移除后: IsInsideTree = False
// 场景 2:queue_free 的"延迟销毁"特性
_itemB.QueueFree();
// queue_free 不会立刻销毁节点!当前帧内 IsInsideTree 仍然是 true
GD.Print($"queue_free 后(同一帧): IsInsideTree = {_itemB.IsInsideTree}");
// 运行结果: queue_free 后(同一帧): IsInsideTree = True
// 要等到当前帧结束,节点才会真正被销毁
// 场景 3:批量筛选——只处理"在职"的节点
var allItems = new Godot.Collections.Array<Node> { _itemA, _itemB, _itemC };
GD.Print("--- 仍在场景树中的节点 ---");
foreach (var item in allItems)
{
if (item != null && item.IsInsideTree)
{
GD.Print($" {item.Name}: 路径 = {item.GetPath()}");
}
}
// 运行结果:
// --- 仍在场景树中的节点 ---
// ItemC: 路径 = /root/DeferredActionDemo/ItemC
// 注意:ItemA 已被 remove_child,ItemB 已被 queue_free(但当前帧还在树中)
// 实际输出取决于 queue_free 的时序
}
}extends Node
@export var ex_delete_delay: float = 2.0
var _item_a: Node
var _item_b: Node
var _item_c: Node
func _ready():
# 创建 3 个道具节点
_item_a = Node.new(); _item_a.name = "ItemA"
_item_b = Node.new(); _item_b.name = "ItemB"
_item_c = Node.new(); _item_c.name = "ItemC"
add_child(_item_a)
add_child(_item_b)
add_child(_item_c)
# 场景 1:定时器回调中做安全检查
# 2 秒后执行回调,但中途可能已经把节点移除了
get_tree().create_timer(ex_delete_delay).timeout.connect(func():
# 定时器触发时,先检查节点是否还在场景树中
if _item_a != null and _item_a.is_inside_tree():
print("定时器触发: %s 还在,执行操作" % _item_a.name)
# 运行结果: 如果没被移除,会走到这里
else:
print("定时器触发: ItemA 已经不在场景树了,跳过")
# 运行结果: 如果中途被移除了,走这个分支
)
# 提前移除 ItemA,模拟"道具被拾走"
remove_child(_item_a)
print("ItemA 被移除后: is_inside_tree = %s" % _item_a.is_inside_tree())
# 运行结果: ItemA 被移除后: is_inside_tree = False
# 场景 2:queue_free 的"延迟销毁"特性
_item_b.queue_free()
# queue_free 不会立刻销毁节点!当前帧内 is_inside_tree 仍然是 true
print("queue_free 后(同一帧): is_inside_tree = %s" % _item_b.is_inside_tree())
# 运行结果: queue_free 后(同一帧): is_inside_tree = True
# 要等到当前帧结束,节点才会真正被销毁
# 场景 3:批量筛选——只处理"在职"的节点
var all_items = [_item_a, _item_b, _item_c]
print("--- 仍在场景树中的节点 ---")
for item in all_items:
if item != null and item.is_inside_tree():
print(" %s: 路径 = %s" % [item.name, item.get_path()])
# 运行结果:
# --- 仍在场景树中的节点 ---
# ItemC: 路径 = /root/DeferredActionDemo/ItemC
# 注意:ItemA 已被 remove_child,ItemB 已被 queue_free(但当前帧还在树中)
# 实际输出取决于 queue_free 的时序注意事项
C# 是属性,GDScript 是方法:C# 中
IsInsideTree是属性,不加括号:node.IsInsideTree;GDScript 中is_inside_tree()是方法,必须加括号:node.is_inside_tree()。忘记加括号会导致 GDScript 报错。新创建的节点默认不在树中:用
new Node()(C#)或Node.new()(GDScript)刚创建的节点,is_inside_tree返回false。必须先调用add_child把它挂到场景树上,才会变成true。queue_free不会立即改变状态:调用queue_free后,节点并不是立刻被销毁的,而是等到当前帧结束才真正移除。所以在同一帧内检查is_inside_tree,返回值仍然是true。如果你需要在回调中判断节点是否已被标记销毁,应该额外维护一个标志位(如_isDestroyed = true)。在信号回调和定时器中务必检查:信号连接的回调函数、定时器的
Timeout回调都是"延迟执行"的。在回调触发时,节点可能已经被移除或销毁了。在这些回调中操作节点前,一定要先用is_inside_tree检查,否则会报错。与
_EnterTree/_ExitTree的关系:_EnterTree回调触发时,is_inside_tree已经是true;_ExitTree回调触发时,is_inside_tree已经变成false。你可以把is_inside_tree理解为进入/退出场景树后的"状态标志",而_EnterTree/_ExitTree是状态变化时的"通知回调"。不能替代 null 检查:
is_inside_tree只能告诉你节点是否在场景树中,不能告诉你节点是否已经被Free()/free()彻底销毁了。已经被销毁的节点,访问它的任何属性都会崩溃。所以在延迟回调中,建议先检查 null,再检查 is_inside_tree:if (node != null && node.IsInsideTree)。C# 命名差异:C# 中使用 PascalCase(
IsInsideTree),GDScript 中使用 snake_case(is_inside_tree())。
