Skip to content
bozar42 edited this page Apr 22, 2020 · 7 revisions

04: Listen Keyboard Inputs

Source code.

At the end of this chapter, your will be able to move pc sprite by arrow keys. Below is the screenshot for Chapter 4.

IMAGE: Chapter 4 screenshot

Respond To Inputs Through Output Messages

Check out commit: 11fbf11.

Godot provides several ways to handle inputs on different levels. The topic is covered in Tutorials/Inputs. Our approach in this demo involves two steps. First register inputs with a specific name in Project Settings/Input Map. Then respond to input events in scripts by implementing _unhandled_input().

Open Input Map (official tutorial). Bind arrow keys and Vi keys (hjkl) to one of four actions: move_left, move_right, move_up and move_down.

Add PCMove (Node2D node) to MainScene node. Attach PCMove.gd to the newly created node. Add InputName.gd to library/ folder to store action names as string constants. Inside PCMove.gd, write code to respond to keyboard inputs by printing messages in the console window.

# InputName.gd

const MOVE_LEFT: String = "move_left"


# PCMove.gd

var _new_InputName := preload("res://library/InputName.gd").new()


func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed(_new_InputName.MOVE_LEFT):
        print("move left")

Try To Move PC

Check out commit: fb307f1.

Moving PC involves three steps:

  • Get a reference to pc node in PCMove.gd.
  • Convert inputs to a pair of integers.
  • Set PC's new position based on the two integers.

Let's solve step two first.

# PCMove.gd

func _unhandled_input(event: InputEvent) -> void:
    var x: int = 0
    var y: int = 0

    if event.is_action_pressed(_new_InputName.MOVE_LEFT):
        x -= 1
    elif event.is_action_pressed(_new_InputName.MOVE_RIGHT):
        x += 1
    elif event.is_action_pressed(_new_InputName.MOVE_UP):
        y -= 1
    elif event.is_action_pressed(_new_InputName.MOVE_DOWN):
        y += 1

In order to get a reference to pc node, we can call get_tree().get_nodes_in_group("pc")[0]. Because according to InitWorld.gd, pc group has only one member, that is, pc node. However, I think this is a good chance to introduce signal. Let InitWorld.gd emit a signal, sprite_created, whenever a new sprite is instanced. PCMove.gd receives and decodes the signal and gets a reference to pc.

# InitWorld.gd

signal sprite_created(new_sprite)


func _create_sprite(prefab: PackedScene, group: String, x: int, y: int,
        x_offset: int = 0, y_offset: int = 0) -> void:

    # Add this line to the end.
    emit_signal("sprite_created", new_sprite)


# PCMove.gd

var _pc: Sprite


func _ready() -> void:
    var __ = get_node("../InitWorld").connect("sprite_created", self,
            "_on_InitWorld_sprite_created")
    print("connect: {0}".format([__]))


func _unhandled_input(event: InputEvent) -> void:
    print("pc: {0}".format([_pc]))

    var source: Array = _new_ConvertCoord.vector_to_array(_pc.position)
    var x: int = source[0]
    var y: int = source[1]

    if event.is_action_pressed(_new_InputName.MOVE_LEFT):
        x -= 1
    elif event.is_action_pressed(_new_InputName.MOVE_RIGHT):
        x += 1
    elif event.is_action_pressed(_new_InputName.MOVE_UP):
        y -= 1
    elif event.is_action_pressed(_new_InputName.MOVE_DOWN):
        y += 1

    _pc.position = _new_ConvertCoord.index_to_vector(x, y)


func _on_InitWorld_sprite_created(new_sprite: Sprite) -> void:
    if new_sprite.is_in_group(_new_GroupName.PC):
        _pc = new_sprite

When testing the game, it turns out that even though connect returns 0, which means that sprite_created is successfully connected with _on_InitWorld_sprite_created(), the reference to pc node is still null. This is due to the fact that PCMove._ready() is called after InitWorld._ready(). The signal is connected to the function, but it is not received even once. One possible solution is to move initialization code from InitWorld._ready() to InitWorld._process().

# InitWorld.gd

var _initialized: bool = false


func _process(_delta) -> void:
    if not _initialized:
        _initialized = true
        _init_floor()
        _init_wall()
        _init_dwarf()
        _init_PC()
        _init_indicator()

The full code at current stage is available at the start of this part. We will take another approach to this problem in the next part.

Press Space To Initialize Game World

Check out commit: aa35961.

PC already responds to arrow keys in the last part. We shall now refactor code to make it more elegant and robust. Instead of initializing game world in InitWorld._process(), we create the dungeon after pressing Space. This includes two tasks.

  • InitWorld._unhandled_input() is active when game starts. It responds to Space key and calls initialization functions. InitWorld no longer reponds to inputs once Space is pressed.
  • PCMove._unhandled_input() is inactive at first. It becomes active once pc sprite is created.

We need to ensure that only one _unhandled_input() is active at a time to avoid potential conflicts.

To fix PCMove.gd is simple.

# PCMove.gd

func _ready() -> void:
    var __ = get_node("../InitWorld").connect("sprite_created", self,
            "_on_InitWorld_sprite_created")
    set_process_unhandled_input(false)


func _on_InitWorld_sprite_created(new_sprite: Sprite) -> void:
    if new_sprite.is_in_group(_new_GroupName.PC):
        _pc = new_sprite
        set_process_unhandled_input(true)

Open Project Settings/Input Map. Bind Space key to action init_world. Also add the action name to res://library/InputName.gd. As for InitWorld, first remove InitWorld._process(). Then move _init functions to another place.

# InitWorld.gd

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed(_new_InputName.INIT_WORLD):
        _init_floor()
        _init_wall()
        _init_PC()
        _init_dwarf()
        _init_indicator()

        set_process_unhandled_input(false)

Connect Signals In Parent Node

A Godot expert (you, for example) would frown at this line of code in PCMove.gd:

# PCMove.gd

var __ = get_node("../InitWorld").connect("sprite_created", self,
    "_on_InitWorld_sprite_created")

It is not a good idea to refer to sibling nodes directly. The official guide suggests:

If a scene must interact with an external context, experienced developers recommend the use of Dependency Injection.

In order to fix this issue, first remove the code above. Attach MainScene.gd to MainScene node. We shall connect signals in the parent node of InitWorld and PCMove.

# MainScene.gd

var __


func _ready():
    __ = get_node("InitWorld").connect("sprite_created", get_node("PCMove"),
            "_on_InitWorld_sprite_created")

The function connect() returns an integer which we are not interested in. We declare a variable __ to receive the return value but we shall never use it. A variable whose name starts with an underscore can be declared but never used. We use these tricks to avoid warning messages.