如何修复 "invalid get index 'position' (on base: Nil)." 错误

how do I fix an "invalid get index 'position' (on base: Nil)." error

在 Godot 中,我正在尝试制作一个简单的 space 射击游戏,玩家必须消灭一定数量的敌人才能进入游戏的下一关。我试图让敌人射击玩家以使游戏更有趣,但我收到一个错误,我不知道如何修复

无效的获取索引 'position'(基于:无)。

我认为这里的问题是变量位置显示为 null 但我不知道如何解决它

脚本:

extends StaticBody2D

var dir = Vector2()
var player
var bullet = preload("res://scebes/EnemyBullet.tscn")

func _ready():
    _get_dir(player)

func _get_dir(target):
    dir = (target.position - position).normalized()


func _on_Timer_timeout():
    _get_dir(player)
    var b = bullet.instance()
    get_parent().add_child(b)
    b.position = position + dir * offset
    b.dir = dir

在线出现错误:

dir = (target.position - position).normalized()

场景树:

Node2D
├ Player
│ └ (...)
├ simple
│ ├ Sprite
│ └ CollisionShape2D
└ enemy
  ├ Sprite
  ├ CollisionShape2D 
  └ Timer

随着Timer信号的超时连接到_on_Timer_timeout

表面诊断

错误告诉您您正在尝试访问 position 上的空值。

行中:

dir = (target.position - position).normalized()

您尝试访问 target (target.position) 的 positionself (position) 的位置。 其中 self 是附加了脚本的对象。 显然 self 不为空。所以,让它必须是 target.

代码获取target作为参数:

func _get_dir(target):
    dir = (target.position - position).normalized()

对症治疗

如果你空检查 target:

,你将摆脱错误

代码获取它作为参数:

func _get_dir(target):
    if target == null:
        return

    dir = (target.position - position).normalized()

或者更好的是,检查 is_instance_valid:

func _get_dir(target):
    if not is_instance_valid(target):
        return

    dir = (target.position - position).normalized()

这样它也将处理目标不为空但它引用已删除节点的情况。 您可以执行更多检查,例如 is_inside_treeis_queued_for_deletion,但通常您不需要这些。

但是,这并没有解决真正的问题。真正的问题是为什么 target 一开始是空的。


更深入的诊断

那么,_get_dir 在哪里调用呢?这里:

func _ready():
    _get_dir(player)

稍后,这里:

func _on_Timer_timeout():
    _get_dir(player)
    # ...

player 从哪里获得它的价值?无处。这才是真正的问题。变量 player 未初始化。


解决方案 1(简单、脆弱)

想必你希望player引用场景树上的Player节点,对吧?

我们需要在 _ready 之前及时完成。所以我们需要在 _ready 的开头插入行,或者在 onready var.

中插入行

这是可能的,尽管它是脆弱的代码:

onready var player := get_node("../Player")

问题在于它假定具有此脚本的节点和 Player 节点是兄弟节点。这在未来可能不是真的。也就是说,重新组织场景树会破坏代码。 因此,我们说代码是脆弱的(容易被破解)。


解决方案 2(复杂、稳健)

我们处于这种情况是因为敌人(我假设敌人中有这段代码)总是知道玩家在哪里。这些是无所不知的敌人。因此,另一种选择是接受它。

您可以创建一个 autoload (singleton)。为此,首先创建一个新脚本(二次单击“文件系统”面板和 select“新脚本...”),如下所示:

var player

也许输入它(Node2DKinematicBody2D 或其他):

var player:Node2D

将其注册为项目设置的自动加载,并使用一些名称,比如 PlayerInfo

现在,玩家需要自己注册:

func _ready() -> void:
    PlayerInfo.player = self

敌人可以这样找到它:

func _ready() -> void:
    player = PlayerInfo.player

情节扭曲:这段代码仍然很脆弱,因为 _ready 执行的顺序取决于场景树。


为避免这种情况,您可以在阅读 PlayerInfo.player:

之前跳过 _ready 中的一帧
func _ready() -> void:
    yield(get_tree(), "idle_frame")
    player = PlayerInfo.player

这样,你确定 Player 有机会设置 PlayerInfo.player


您也可以在_enter_tree中注册播放器:

func _enter_tree() -> void:
    PlayerInfo.player = self

对于给定节点,_enter_tree 方法会在 _ready 之前 运行。但不一定在另一个节点的_ready之前。


您可以做的另一件事是摆脱 player 变量并直接使用 PlayerInfo.player

_get_dir(PlayerInfo.player)

所以即使代码第一次得到null,它也不会卡在那个null上。

顺便说一下,您可以从任何节点使用 PlayerInfo.player


对了,你还记得额外的支票吗?你可以把它们放在自动加载中:

var player:Node2D setget , get_player

func get_player() -> Node2D:
    if not is_instance_valid(player):
        # player is null, or deleted
        return null

    if not player.is_inside_tree():
        # player has not been added or has been removed form the scene
        return null

    if player.is_queued_for_deletion():
        # player is queued for deletion (e.g. queue_free)
        return null

    return player

每次 PlayerInfo.player 被读取时 运行。


注意:还有其他原因需要自动加载以某种方式引用播放器。例如,您可以将保存和加载游戏功能放在那里。或者它可以帮助您将玩家从一个场景移动到另一个场景。因此,这种额外的复杂性可能在大型游戏的长 运行 中被证明是有用的。