2

Multimesh has StaticBody and CollisionBody. I have this script on my Multimesh that makes an array of objects in a row. (Fence for example)

tool
extends MultiMeshInstance

export (float) var distance:float = 1.0 setget set_distance
export (int) var count:int = 1 setget set_count
export (Mesh) var mesh:Mesh setget set_mesh
export (Vector3) var rotMesh:Vector3 setget set_rotMesh
export (Vector3) var sclMesh:Vector3 setget set_sclMesh
export (Vector3) var colMesh:Vector3
onready var coll = get_node("static/collision")

func set_distance(new_distance):
    distance = new_distance
    update()
func set_count(new_count):
    count = new_count
    update()
func set_mesh(new_mesh):
    mesh = new_mesh
    update()
func set_rotMesh(new_rot):
    rotMesh = new_rot
    update()
func set_sclMesh(new_scl):
    sclMesh = new_scl
    update()

func update():
    self.multimesh = MultiMesh.new()
    self.multimesh.transform_format = MultiMesh.TRANSFORM_3D
    self.multimesh.instance_count = count
    self.multimesh.visible_instance_count = count
    self.multimesh.mesh = mesh
    var offset = Vector3(0,0,0)
    var trfMesh:Basis = Basis(rotMesh)
    var extents = Vector3(colMesh.x*distance*count,colMesh.y,colMesh.z)
    var shape:Shape = BoxShape.new()
    shape.extents = extents
    coll.Shape = shape # or coll.shape the same error
    trfMesh = trfMesh.scaled(sclMesh)
    for i in range(count):
        self.multimesh.set_instance_transform(i, Transform(trfMesh, offset))
        offset.x += distance

I want to set CollisionShape automatically, or at least by some params through my script.

When I trying to set CollisionShape, I get:

res://scene/test.gd:40 - Invalid set index 'Shape' (on base: 'Nil') with value of type 'BoxShape'.
  • I solve my problem using MeshDataTool and call get_node() in update(). But when I save my script I still get errors: p_mesh.is_null() is true - https://github.com/godotengine/godot/issues/19517 res://scene/test.gd:46 - Invalid set index 'transform' (on base: 'null instance') with value of type 'Transform'. how can I suppress or solve the second error? (This is doesn't affect Real MultiInstance, but I don't like any errors) – Kirill Moskalew Apr 04 '21 at 23:59

1 Answers1

3

During scene load, Godot will follow this execution order (new version):

  1. Allocate the new node, all variables are zeroed. regular variables (no onready) are initialized here to their default value (if they are export, the value is overwritten on step 3, custom setter or not), zeroed if no default value is specified.
  2. Call _init on it.
  3. Set its properties (initialize export variables, and any custom setters run).
  4. If there are nodes that should be children, yet to do steps 1 to 3, follow this same steps for them.
  5. IDE signal connections are made (this happens after all nodes have gone though steps 1 to 3, and yes, that includes connections from and to future children).
  6. Add the node to its parent.
  7. NOTIFICATION_PARENTED (18).
  8. NOTIFICATION_ENTER_TREE (10).
  9. Send tree_entered signal.
  10. If there are nodes, that should be children, yet to do steps 6 to 9, follow this same steps for them.
  11. NOTIFICATION_POST_ENTER_TREE (27).
  12. Initialize any onready variables.
  13. Call _ready on it.
  14. NOTIFICATION_READY (13).
  15. Send ready signal.

One important caveat is that NOTIFICATION_ENTER_TREE and tree_entered happen if the parent is in the SceneTree (e.g. if the node is created from script and yet to be added), also they do not happen in the editor (for tool scripts). Speaking of tool scripts, the steps from 11 on do not happen in the editor. Basically ready and enter_tree don't work on the editor.

See also:


Thus, when your setters call update (step 3 above), this line is yet to run (it will run on step 12):

onready var coll = get_node("static/collision")

Because of that, coll is null Nil at:

coll.Shape = shape # or coll.shape the same error

And of course, Godot cannot access Shape of Nil.


A common solution is to follow this pattern:

func set_distance(new_distance):
    distance = new_distance
    if not is_inside_tree():
        yield(self, "ready")

    update()

This means that when the setter is called, but the node is not yet in the scene tree, Godot will halt the execution until it gets the ready signal (which happens after the node is in the scene tree, and onready variables are initialized).


However, there is another problem! This is a tool script!

When running in the editor, onready and ready don't work, plus is_inside_tree will return true. Thus, in the editor, it will call update but coll is Nil.

You can use Engine.editor_hint to prevent the setters to call update in the editor, like this:

func set_distance(new_distance):
    distance = new_distance
    if Engine.editor_hint:
        return

    if not is_inside_tree():
        yield(self, "ready")

    update()

Remember that for a tool script you cannot rely on onready. I suggest to use get_node_or_null and check for null. So you could do something like this:

func set_distance(new_distance):
    distance = new_distance
    coll = get_node_or_null("static/collision")
    if coll == null:
        return

    update()

This way, when Godot calls the setter during initialization it will fail to find the node, and not call update. You may also put that check inside update, of course.

We can do a bit better. During runtime Godot will call the setter... So we will yield to have the execution continue after ready, at which point it should find the node, unless something has gone wrong.

func set_distance(new_distance):
    distance = new_distance
    if not Engine.editor_hint and not is_inside_tree():
        yield(self, "ready")

    coll = get_node_or_null("static/collision")
    if coll == null:
        return

    update()

We might notify if something has gone wrong:

func set_distance(new_distance):
    distance = new_distance
    if not Engine.editor_hint and not is_inside_tree():
        yield(self, "ready")

    coll = get_node_or_null("static/collision")
    if coll == null:
        if not Engine.editor_hint:
            push_error("static/collision not found")

        return

    update()

Extract to another function:

func set_distance(new_distance):
    distance = new_distance
    if not Engine.editor_hint and not is_inside_tree():
        yield(self, "ready")

    if can_update():
        update()

func can_update() -> bool:
    coll = get_node_or_null("static/collision")
    if coll == null:
        if not Engine.editor_hint:
            push_error("static/collision not found")

        return false

    return true

You may reuse that extracted function in all your setters in the same pattern. Adapt it to whatever make sense on the script at hand. Here it only cares about the node that you use in update, but you can make it whatever logic you need. And, yes, you could also do it inside update proper.


There is something that has become somewhat a common practice: Exporting a bool variable to work as an update button.

We can do that with the pattern I described above:

export (bool) var refresh:bool setget set_refresh
func set_refresh(new_value):
    refresh = new_value
    if not Engine.editor_hint and not is_inside_tree():
        yield(self, "ready")

    if can_update():
        update()

    refresh = false

That will add a Refresh atribute on the inspector panel, with a checkbox that you can click. Resulting in a call to update if possible. Plus, the checkbox remains unchecked. This refresh is always false.

The idea being that you can click it in the editor to call update if needed.

Additionally, this setter will also execute during runtime, and call update soon after initialization. You can have it only do something on the editor:

export (bool) var refresh:bool setget set_refresh
func set_refresh(new_value):
    refresh = new_value
    if Engine.editor_hint and can_update():
        update()

    refresh = false

You can add _get_configuration_warning to provide a warning that will appear in the scene panel (Similar to how a PhysicsBody tells you it needs a CollisionShape or CollisionPolygon):

func _get_configuration_warning():
    if can_update():
        return ""

    return "static/collision not found"
Theraot
  • 31,890
  • 5
  • 57
  • 86