Search code examples
godotgdscriptgodot4

Godot4 - How can I reference the property of a preloaded scene in the _ready() method of the scene it is preloaded in?


I am a Godot beginner and currently trying to replicate the famous snake game. In the _ready() method of the snake_manager scene (Code below), I want to set the global_position of the preloaded scene "snake_head_segment" and "snake_body_segment". My problem now is that the global_position property of the snake_head_segment is null when I try to set its position. I get this error: Invalid set index 'global_position' (on base: 'CharacterBody2D (snake_head.gd)') with value of type 'Nil'. The code of the snake_manager scene:

extends Node

var snake_body_segment: PackedScene = preload("res://scenes/gameobjects/snake_segment/snake_segment.tscn")
var snake_head_segment: PackedScene = preload("res://scenes/gameobjects/snake_head/snake_head.tscn")
var snake_segments_list = [snake_head_segment, snake_body_segment, snake_body_segment]


func _ready():
    var body_part
    for x in range(0, len(snake_segments_list)):
        body_part = snake_segments_list[x].instantiate()
        snake_segments_list[x] = body_part
        get_parent().add_child.call_deferred(body_part)
        snake_segments_list[x].global_position = get_segment_position(snake_segments_list, x)


func _process(_delta):
    for x in range (1, len(snake_segments_list)):
        var tween = get_tree().create_tween()
        tween.tween_property(snake_segments_list[x], "position", get_segment_position(snake_segments_list, x), .5)


func get_segment_position(snake_segments, x):
        if snake_segments[0].velocity.x != 0:
            if snake_segments[0].velocity.normalized() == Vector2.RIGHT:
                return Vector2(snake_segments[x - 1].global_position.x - 40, snake_segments[x - 1].global_position.y)
            elif snake_segments[0].velocity.normalized() == Vector2.LEFT:
                return Vector2(snake_segments[x - 1].global_position.x + 40, snake_segments[x - 1].global_position.y)
        elif snake_segments[0].velocity.y != 0:
            if snake_segments[0].velocity.normalized() == Vector2.DOWN:
                return Vector2(snake_segments[x - 1].global_position.x, snake_segments[x - 1].global_position.y - 40)
            elif snake_segments[0].velocity.normalized() == Vector2.UP:
                return Vector2(snake_segments[x - 1].global_position.x, snake_segments[x - 1].global_position.y + 40)

I would really appreciate your help and thank you in advance.

Kind regards, Gabriel

I checked with breakpoints which _ready() method is called first and found out that the _ready() method of the snake_manager (the parent node) is called before the _ready() method of the snake_head_segment scene (child node).That's probably the reason for the error.


Solution

  • This is riddled with issues.

    First I need to point out tha the type of elements of snake_segments_list changes from PackedScene to CharacterBody2D. This not make it easier to reason about the code.


    While reading the code the first potential problem I saw was that you would be setting global_position before add_child gets to run. See my answer to the question Why isnt my spike shooters child showing up in the scene in godot? to find out why that might be an issue.

    However, that is a potentially unexpected behavior, and the execution is not even getting there...


    The next thing I noticed is the following...

    You are making a loop where x will be in the range from 0 to the size of snake_segments. On each iteration you are calling get_segment_position passing x.

    This means that the first time you call it, x will be 0. And so the lines where you access snake_segments[x - 1] are reading the index -1.

    In Godot, reading the -1 index of an Array will give you the last item of the Array. However, the loop has not ran for the last item, so it has not been instantiated.

    In fact, the last item would be a PackedScene, (although you expect it to be CharacterBody2D. And no, PackedScene does not have a global_position property.

    ...But this isn't the error either. Because the execution code is not even getting to the point to try to read the global_position of a PackedScene, because the the first element of snake_segments (i.e. the head) has zero velocity.


    And that brings us to the problem at hand: When the head has zero velocity get_segment_position does not return.

    Thus in this line:

    snake_segments_list[x].global_position = get_segment_position(snake_segments_list, x)
    

    It acts as a null, and you can't set a null to the global_position of a CharacterBody2d. You have to set a Vector2. Hence the error.

    If you had declared the return value of get_segment_position, Godot would have pointed out that it didn't always return.


    Right, let us talk about fixing it. Presumably if the velocity of the head is zero, you don't want the global_position of the segments to change, so you can add a line at the end of get_segment_position to return the current global_position of the segment.