Wednesday 10 May 2017

Gitting Gud At Godot - Part 4 - Input

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

In this tutorial, I'm going to explain how to add some input handling to your game. I think it's safe to say that most of you will want to know about this one!

Godot has a few options for handling input in different ways. By far the easiest way is to use the built-in input mapping. In the top left of the editor, go to Scene->Project Settings. This will open a bunch of technical stuff which you can change, but it isn't vital for this bit.


Select the "Input Map" tab along the top. You'll see a few headings such as "ui_accept", "ui_select", etc. Underneath each heading you'll notice several icons and descriptions of key/controller presses associated with them.

This is one of my favourite things about Godot. The headings are called "actions", and you can bind multiple input methods to a single action. Let's create a new action and call it "move_right". You can do this by typing "move_right" into the text bar at the top, then press the "Add" button to the right.

Scroll down to the bottom of the list, and you should see an action titled "move_right". Press the "+" on the right hand side of that field and select "Key" and press the D key.

Repeat this for "move_up", "move_down", and "move_left", binding them to the W, S and A keys, respectively. Standard WASD control scheme. If you really want to, you could bind the appropriate arrow keys to each action as well.


Once we've done this, we can press "Close" and return to the editor. Click the script icon on the "player" node to open the script editor.

Our player.gd file currently looks like this:

extends Sprite

func _ready():
    set_process(true)

func _process(delta):
    var current_position = get_pos()
    current_position.x += 1
    set_pos(current_position)

Currently, our sprite just moves to the right automatically. However, we want the sprite to only move when a key is being pressed. Fortunately, Godot has us covered. We can access the function "Input.is_action_pressed(action)", which will return a boolean value as to whether the action specified is being pressed down.

In short, we can make it so that the sprite only moves when the move_right action is being pressed. This is how:

extends Sprite

func _ready():
    set_process(true)

func _process(delta):
    var current_position = get_pos()
    if(Input.is_action_pressed("move_right")):
        current_position.x += 1
    set_pos(current_position)

And that's all you need to do! It's simple to expand this to also cover moving in the other directions, but for completeness I'll put the code below.

extends Sprite

func _ready():
    set_process(true)

func _process(delta):
    var current_position = get_pos()
    if(Input.is_action_pressed("move_right")):
        current_position.x += 1
    elif(Input.is_action_pressed("move_left")):
        current_position.x -= 1
    if(Input.is_action_pressed("move_up")):
        current_position.y -= 1
    elif(Input.is_action_pressed("move_down")):
        current_position.y += 1   
    set_pos(current_position)

You can now launch your project, and it should work. Hooray!! Truly an award-winning user experience.

However, we're not done yet. There is another way to handle input in Godot, using event-based programming.

We can make Godot start to process input by putting a single statement in our _ready() function, "set_process_input(true)". I'm sure you can guess what this does.

Next, define a new function called _input(event). This function will be called every time the user puts some input into our game, and pass the "event" argument. "event" is an object of type InputEvent which contains a lot of data about the input event, but we won't be needing all of it for this exercise. We're going to implement a teleport ability, so when we click the mouse the sprite object will teleport to that location.

Under _input(event), we need to check to make sure that the event that's being passed really is a mouse button click. This isn't too hard, though I would have some documentation ready because some of these identifiers aren't intuitive.

func _input(event):
    if(event.type == InputEvent.MOUSE_BUTTON):
        print("Click!")

event.type is a numerical constant between 0 and 8 which will tell us what kind of action it is. Similarly, InputEvent.MOUSE_BUTTON is actually a constant that represents the number 3. However, typing out if(event.type == InputEvent.MOUSE_BUTTON) makes the code a lot more clear than if we had just typed if(event.type == 3).

You can start up your project now and you'll be able to see that whenever you click, "Click!" should show up in your output console. However, you might also notice that it outputs a "Click!" for both pressing and releasing the mouse button. We need to make it so that we're only dealing with one "Click!" each time the user presses and releases the mouse button. Fortunately for us, this is also very easy.

func _input(event):
    if(event.type == InputEvent.MOUSE_BUTTON and event.is_pressed()):
        print("Click!")

Start up your project again, and hopefully you'll only be getting one "Click!" per mouse press.

However, we're still not done yet, and this is about to get a little more complicated. Right clicking will also trigger a "Click!", and we only want to deal with the left mouse button.

I need to come clean here, I've lied a little bit. Earlier, I told you that  "event" is an object of type InputEvent. This isn't strictly true. In the case of our event of InputEvent.MOUSE_BUTTON, we are actually dealing with an object of type InputEventMouseButton. If we were dealing with key presses, it would be of type InputEventKey. However, let's just focus on InputEventMouseButton.

InputEventMouseButton has a member variable named "button_index". This is an integer between 0 and 7, corresponding to 8 different buttons that it could be. This is similar to our "event.type" from earlier. You can find a full list of IDs here: http://docs.godotengine.org/en/stable/classes/class_@global%20scope.html, but don't be intimidated because you will only ever need these on a case-by-case basis.

All we need to focus on is BUTTON_LEFT, which is a globally defined constant. This means we can do the following:

func _input(event):
    if(event.type == InputEvent.MOUSE_BUTTON and event.is_pressed()):
        if(event.button_index == BUTTON_LEFT):
            print("Click!")

We need to indent the second check, since this will throw an exception if we're trying to check for button_index on a type InputEventKey.

I encourage you to re-read this section if it confused you, since I'm aware it's not the most clear thing in the world. Hell, you could probably stick to using the polling method from before and remain comfortable for a while.

However, we're still not done. We haven't actually teleported anything anywhere. We've sent "Click!" to the output a hell of a lot, but so far we've done no teleporting. From here, this is pretty simple.

func _input(event):
    if(event.type == InputEvent.MOUSE_BUTTON and event.is_pressed()):
        if(event.button_index == BUTTON_LEFT):
            set_pos(event.pos)

Hooray! We've done it! Hopefully that wasn't too awful. Documentation is your best friend, trust me.

Thanks for reading if you've not dropped dead, stay tuned for part 5 where I'll be covering the less grizzly collision detection!

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

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

Addendum

For some reason, Firefox doesn't recognize teleport as being a real word. Strange.

4 comments :

  1. Good tutorial! But it would be better if instead of using set_pos(), you used translate()!
    It's like KinematicBody2D's move() for all objects inheriting from Node2D.

    ReplyDelete
  2. Again godot has changed. It seems the global scope ids are now listed here:
    https://docs.godotengine.org/en/stable/classes/class_@globalscope.html?highlight=button_left

    The mouse handling seems to have changed a lot, but I found the following tutorial helpful
    http://docs.godotengine.org/en/stable/tutorials/inputs/mouse_and_input_coordinates.html

    So the code becomes something like the following (Sorry, I deleted my working function in the next tutorial):

    func _input(event):
    # Mouse in viewport coordinates
    if (event is InputEventMouseButton and event.is_pressed()):
    if(event.button_index == BUTTON_LEFT):
    set_position(event.position)

    print("Mouse Click/Unclick at: ", event.position)

    ReplyDelete
  3. I am looking at Unknown's comments, they are pretty helpful.

    I have Godot 3.0.6, and instead of move_right, move_left, move_up and move_down, I have ui_right, ui_left, ui_up and ui_down.

    Instead of:

    func _input(event):
    if(event.type == InputEvent.MOUSE_BUTTON):
    print("Click!")

    This worked for me:

    func _input(event):
    if event is InputEventMouseButton:
    print("Click!")


    func _input(event):
    if event is InputEventMouseButton and event.is_pressed():
    print("Click!")

    ReplyDelete
  4. extends Sprite

    func _ready():
    set_process(true)
    set_process_input(true)


    func _process(delta):
    var current_position = get_position()
    if(Input.is_action_pressed("ui_right")):
    current_position.x+=1
    elif(Input.is_action_pressed("ui_left")):
    current_position.x-=1
    if(Input.is_action_pressed("ui_up")):
    current_position.y-=1
    elif(Input.is_action_pressed("ui_down")):
    current_position.y += 1
    set_position(current_position)


    func _input(event):
    if event is InputEventMouseButton and event.is_pressed():
    if(event.button_index == BUTTON_LEFT):
    set_position(event.position)
    print("Mouse Click/Unclick at:", event.position)

    ReplyDelete