如何为Godot制作Multimesh构建脚本?

how to make Multimesh construction script for Godot?

Mu​​ltimesh 有 StaticBody 和 CollisionBody。 我的 Multimesh 上有这个脚本,它使一组对象连续排列。 (例如栅栏)

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

我想自动设置 CollisionShape,或者至少通过我的脚本设置一些参数。

当我尝试设置 CollisionShape 时,我得到:

res://scene/test.gd:40 - Invalid set index 'Shape' (on base: 'Nil') with value of type 'BoxShape'.

在场景加载期间,Godot 将遵循以下执行顺序(new version):

  1. 分配新节点,所有变量都归零。常规变量(没有onready)在这里初始化为它们的默认值(如果它们是export,该值在第 3 步被覆盖,自定义 setter 或不),如果未指定默认值,则归零。
  2. 调用_init就可以了。
  3. 设置其属性(初始化export variables, and any custom setters运行)。
  4. 如果有应该是子节点但尚未执行步骤 1 到 3 的节点,请对它们执行相同的步骤。
  5. IDE 建立信号连接(这发生在所有节点都完成步骤 1 到 3 之后,是的,这包括未来子节点的连接)。
  6. 将节点添加到其父节点。
  7. NOTIFICATION_PARENTED (18).
  8. NOTIFICATION_ENTER_TREE (10).
  9. 发送tree_entered信号。
  10. 如果有节点,应该是子节点,但还没有执行步骤 6 到 9,请对它们执行相同的步骤。
  11. NOTIFICATION_POST_ENTER_TREE (27).
  12. 初始化任何onready variables
  13. 调用_ready就可以了。
  14. NOTIFICATION_READY (13).
  15. 发送ready信号。

一个重要的警告是 NOTIFICATION_ENTER_TREEtree_entered 如果父级在 SceneTree 中(例如,如果节点是从脚本创建但尚未添加),也会发生它们不会出现在编辑器中(对于 tool 脚本)。说到 tool 脚本,从 11 开始的步骤不会在编辑器中发生。 基本上 readyenter_tree 在编辑器上不起作用

另请参阅:


因此,当您的 setter 调用 update(上面的第 3 步)时,此行尚未 运行(它会 运行 在第 12 步):

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

因此,collnull Nil 在:

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

当然,Godot 无法访问 NilShape


一个常见的解决方案是遵循这种模式:

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

    update()

这意味着当 setter 被调用时,但节点还不在场景树中,Godot 将停止执行,直到它获得 ready 信号(发生在节点之后在场景树中,onready个变量被初始化)。


但是,还有一个问题!这是一个tool脚本!

在编辑器中运行ning时,onreadyready不起作用,加上is_inside_tree会returntrue。因此,在编辑器中,它将调用 updatecollNil.

您可以使用 Engine.editor_hint 来阻止 setter 在编辑器中调用 update,如下所示:

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

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

    update()

请记住,对于 tool 脚本,您不能依赖 onready。我建议使用 get_node_or_null 并检查 null。所以你可以这样做:

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

    update()

这样,Godot在初始化时调用setter时会找不到节点,不会调用update当然你也可以把那张支票放在update里面。

我们可以做得更好。在 运行 时间 Godot 将调用 setter... 所以我们将 yieldready 之后继续执行,此时它应该找到节点,除非有什么出错了。

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()

如果出现问题,我们可能会通知:

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()

提取到另一个函数:

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

您可以在所有 setter 中以相同的模式重用提取的函数。使其适应手头脚本的任何有意义的内容。这里它只关心你在 update 中使用的节点,但你可以根据需要制作任何逻辑。而且,是的,您也可以在 update 内部进行。


有些事情已经成为一种常见的做法:导出一个 bool 变量作为更新按钮。

我们可以用我上面描述的模式来做到这一点:

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

这将在检查器面板上添加一个 Refresh 属性,带有一个您可以单击的复选框。如果可能,会调用 update。另外,该复选框保持未选中状态。这个 refresh 总是错误的。

想法是您可以在编辑器中单击它以在需要时调用 update

此外,这个setter也会在运行时间内执行,并在初始化后不久调用update。你可以让它只在编辑器上做一些事情:

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

您可以添加 _get_configuration_warning 以提供将出现在场景面板中的警告(类似于 PhysicsBody 告诉您它需要 CollisionShapeCollisionPolygon 的方式):

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

    return "static/collision not found"