Tuesday 16 May 2017

Gitting Gud At Godot - Part 7 - Instancing

Watch the video version of this tutorial here: https://www.youtube.com/watch?v=05eMjKLua9Y

In this tutorial, I'm going to explain instancing. This topic isn't super hard, so prepare for a little break from the more intensive stuff!


In order to have something to instance, we need to create a new scene. Instead of making the tree root of type Node, let's make the tree root of type KinematicBody2D. Let's give it the appropriate other parts, such as a Sprite and a fitting CollisionShape2D as children of the tree root.

Also, I highly recommend that you leave all the components at the origin. This means that when we instance this node in our "mainscene" scene, it'll be easier to manipulate it.


Let's save this scene and call it "the_absolute_crazy_boy_who_moves_and_isnt_afraid_of_anything.tscn".

No? Really? Fine, let's call it "mover.tscn". While we're at it, let's name our tree root "mover" too. Next, let's add a script to our tree root, and save it as "mover.gd".

Let's make this node move constantly towards the right, by typing this.

extends KinematicBody2D

func _ready():
    set_process(true)

func _process(delta):
    move(Vector2(1, 0))

Now that we've done this, our scene is set up. Let's return to the mainscene.tscn tab along the top just below the play button.

Next, we can add our mover prefab to the main scene. You can do this by selecting the button that looks like a chain just to the right of the + button where you would normally add nodes. Make sure that your tree root is selected!

From here, you should be able to see that the "mover" object is visible in both your scene tab and in the scene viewer itself. We can move the mover somewhere into the window.


Press play, and we should be able to see that our mover node does indeed move! Hooray, we've successfully instanced something!

However, what if we want to do it programmatically?

Delete the "mover" node from the scene tab.

Now, let's open our "player" script. Underneath what we already have, we can start typing the code to instance our mover.

First, we need to load the "mover.tscn" resource. This can be done with our handy friend "load()". load() takes in a file path(from res://) and returns a packed resource which we can then instance.

func _on_collectable_body_enter( body ):
    if(not(wall_destroyed)):
        get_node("../KinematicBody2D").queue_free()
    
    var mover_resource = load("res://mover.tscn")
    var mover = mover_resource.instance()

In this code snippet, I've defined mover_resource as being the resource that I want to make an instance of and then I've defined mover as that instance. Also, I've removed the speed increase line because it was causing a bit of clutter.

However, if you run this you'll notice that the mover does not appear when you move over the Area2D. That's because we haven't actually added it to the scene yet. This is fairly simple, we just have to do:

func _on_collectable_body_enter( body ):
    if(not(wall_destroyed)):
        get_node("../KinematicBody2D").queue_free()
    
    var mover_resource = load("res://mover.tscn")
    var mover = mover_resource.instance()
    get_node("../../Node").add_child(mover)

This extra line gets the node "Node" - also known as the tree root - and adds "mover" as a child. Now, we can move over the Area2D and see that our mover appears. Pretty good stuff!

However, it appears at the origin and half of it is off the top of the screen. Great. This is easily fixed by adding the line "mover.set_pos(Vector2(150, 300))" or some coordinate that you like. If you're feeling especially adventurous, you could even play around with the function randi()%<max_value>!

One more thing that you might notice here is that the movers themselves can spawn more movers. This is because the Area2D detects all bodies that pass through it. The fix is pretty simple, since the body that passes through the Area2D is passed as an argument. We can simply compare it with the "self" keyword, which refers to the object that the script is attached to. In this case, this is our player.

func _on_collectable_body_enter( body ):
    if(not(wall_destroyed)):
        get_node("../KinematicBody2D").queue_free()
    
    if(body==self):
        var mover_resource = load("res://mover.tscn")
        var mover = mover_resource.instance()
        get_node("../../Node").add_child(mover)
        mover.set_pos(Vector2(100, 100))

Instancing like this has a bunch of applications, from enemies to bullets to fancy effects. It's some very useful stuff, and I hope it all made sense!

Thanks for reading, and stay tuned for part 8 on the topic of animations!

Part 6: http://alexhoratiogamedev.blogspot.com/2017/05/gitting-gud-at-godot-part-6.html

4 comments :

  1. Great tutorial again! You really did it! (two days two tutorials). One little thing: I think that instead of
    "if(body!=self):" should be "if(body==self):" (It's in your last block of code). Thanks again! Now I'm going to make Pong!

    ReplyDelete
    Replies
    1. Eek, good catch, I've since updated it. Thanks!

      Also, best of luck with Pong!!

      Delete
  2. ```
    get_node("../../Node").add_child(mover)
    ```

    ```
    get_node("..").add_child(mover)
    ```

    Also I'm wondering about the necessity of making our scene have a Node2D of "root". After looking at `get_path()` from the Node2D callbacks, it seems like the scene ,or maybe just the game itself, by default has a "root" node. So inside of player, a `print(get_path())` returns "/root/root/player". It might still have some organizational advantage to keep the custom root node on-top of the built-in one, but I'm unsure. Thoughts?

    ReplyDelete
  3. You now have to do mover.set_position rather than set_pos.

    ReplyDelete