Pluto
Pluto

Reputation: 3

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

In Godot I am trying to make a simple space shooter game where the player has to destroy a certain amount of enemies to get to the next level of the game. I am trying to get the enemy's to shoot the player to make the game a bit more fun but I am getting an error that I have no idea how to fix

invalid get index 'position' (on base: Nil).

I think the problem here is that the variable position is showing up as null but I don't know how to fix it

Script:

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

With the error appearing on the line:

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

Scene tree:

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

With the timeout of the Timer signal connected to _on_Timer_timeout.

Upvotes: 0

Views: 11659

Answers (1)

Theraot
Theraot

Reputation: 40285

Superficial Diagnose

The error is telling you that you are trying to access position on something that is null.

In the line:

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

You try to access position of target (target.position) and position of self (position). Where self is the object that has the script attached. Evidently self is not null. So, let it has to be target.

The code gets target as parameter:

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

Symptomatic Treatment

You would get rid of the error if you null check target:

The code gets it as parameter:

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

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

Or better yet, check with is_instance_valid:

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

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

That way it will also handle the case where target is not null, but it references a deleted node. There are more checks you could do such as is_inside_tree or is_queued_for_deletion, but usually you don't need those.

However, that does not get rid of the real problem. The real problem is why target is null to begin with.


Deeper Diagnose

So, where is _get_dir called? Here:

func _ready():
    _get_dir(player)

And later, here:

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

And where does player gets it value? Nowhere. That is the real problem. The variable player is not initialized.


Solution 1 (Simple, Fragile)

Presumably you want player to reference the Player node on the scene tree, right?

And we need to do it just in time for _ready. So we need to either insert lines at the start of _ready, or do it in an onready var.

This is possible, even though, it is fragile code:

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

The problem with that is that it assumes that the node with this script and the Player node are siblings. Which might not be true in the future. That is, reorganizing the scene tree would break the code. Hence, we say the code is fragile (easy to break).


Solution 2 (Complex, Robust)

We are in this situation becase the enemy (I'm assuming this code in the enemy) always knows where the player is. These are omniscient enemies. Thus, another option is to embrace that.

You can create an autoload (singleton). To do that, begin by creating a new script (secondary click on the FileSystem panel and select "New Script...") that looks like this:

var player

Perhaps type it (Node2D, KinematicBody2D or whatever):

var player:Node2D

Register it as an autoload on project settings with some name, let us say PlayerInfo.

Now, the player needs to register itself:

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

And the enemy can find it like this:

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

Plot twist: this code is still fragile, because the order in which _ready execute depends on the scene tree.


To avoid that, you can skip a frame in _ready before reading PlayerInfo.player:

func _ready() -> void:
    yield(get_tree(), "idle_frame")
    player = PlayerInfo.player

That way, you are sure Player had a chance to set PlayerInfo.player.


You may also register the player in _enter_tree:

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

The _enter_tree method would run before _ready for a given node. But not necessarily before _ready of another node.


Another thing you can do is get rid of the player variable and use PlayerInfo.player directly:

_get_dir(PlayerInfo.player)

So even if the code got null the first time, it would not be stuck with that null.

By the way, you could use PlayerInfo.player from any node.


By the way, do you remember the extra checks? you could have them in the autoload:

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

Which would run every time PlayerInfo.player is read.


Note: there are other reasons to have an autoload that references the player in some way. For example, you could put save and load game functionality there. Or it can help you move the player from one scene to another. Thus, this extra complexity may proof useful in the long run, for big games.

Upvotes: 3

Related Questions