Logo

Braindead.bzh

Game Dev Hobbyist

A blog about my game dev hobby and various stuff.

Menu
Logo

Braindead.bzh

Game Dev Hobbyist

A blog about my game dev hobby and various stuff.

Creating a game with Godot Engine - Ep.4 - User Interaction

In this fourth article we will talk about ways how to create interaction with the player. After looking into what is the Input Map, we will see how to dynamically rebind keys. Then we will go through a general overview of how user interface works in Godot. We will also talk about signals. Finally, we will use all this to explain how the main menu is done. (source code)

Disclaimer: the following article represents one way to do it and, it may not be the best way. Please adapt this technique using your best judgement.

Introduction

Without interaction your project would not be a game but a movie. This is why user interaction is very important, you can have the most beautiful graphics and the best physic engine, if your user interface or controls are crappy, the game will not be enjoyable.

Input Map

One main interaction is being able to press keys, to control a character for example. With Godot you have two ways, you can do it by detecting key press in your scripts and reacting accordingly. Or you can do it the proper way by using the Input Map. What the Input Map allows you do is to create actions, like move_up or display_debug, and assign to them one or more user inputs. An user input can be a key, a mouse button, a joypad button or axis. Then in your script instead of detecting key press you detect actions which makes for a very flexible approach. Example:

func _input(event):
    if event.is_action("game_quit"):
        _load_screen("main_menu")
    elif event.is_action("display_debug") && event.is_pressed():
        fps.set_hidden(!fps.is_hidden())

There are two advantages by using the Input Map:

  1. You can assign multiple user inputs, like two keys and a joypad button, to the same action very easily.
  2. You can change these inputs quickly without changing a line of code, and even dynamically.

Key rebinding

A very important aspect of the user interaction is to let the player customize his/her experience. For example, I am using a french keyboard (i.e. AZERTY), meaning that the standard WASD is not working for me. In many games, I end up having difficulties to play because I am not offered the possibility the rebinding my keys. However, using the Input Map and the previously created User Settings, you can do it easily with Godot.

Here is the code to load key bindings from the settings:

func _load_key_bindings():
    var bindings = _user_settings.get_section_keys(KEY_BINDING_SECTION)
    for binding in bindings:
        InputMap.erase_action(binding)
        InputMap.add_action(binding)
        var inputs = _user_settings.get_value(KEY_BINDING_SECTION, binding)
        for input in inputs:
            InputMap.action_add_event(binding, input)

After retrieving all the bindings from the settings, we go through all of them. For each binding, we clear the corresponding action by calling erase_action and add_action, to avoid conflicts and duplicates. Then we retrieve all the inputs for the action. For each, input we create the binding by simply calling action_add_event.

Here the code to dynamically replace a key binding:

func replace_keybinding(action, input, scancode):
    var current = InputMap.get_action_list(action)
    var event = InputEvent()
    event.type = InputEvent.KEY
    event.scancode = scancode
    current[input] = event
    _user_settings.set_value(KEY_BINDING_SECTION, action, current)
    save_settings()
    _load_key_bindings()

First we retrieve the current bindings for the given action. Then we create the key event from the new key scan code. After, we replace in the current bindings the event for the input being modified. Then we update the user settings with the new value. Finally we reload the key bindings to take the modification into account.

When the user settings file is created, you have to populate it with some bindings for the previous code to be working. Here the code on how to create the default values from the Input Map for the move_up, move_down, move_left and move_right actions:

func _create_default_key_bindings():
    _user_settings.set_value(KEY_BINDING_SECTION, "move_up", InputMap.get_action_list("move_up"))
    _user_settings.set_value(KEY_BINDING_SECTION, "move_down", InputMap.get_action_list("move_down"))
    _user_settings.set_value(KEY_BINDING_SECTION, "move_left", InputMap.get_action_list("move_left"))
    _user_settings.set_value(KEY_BINDING_SECTION, "move_right", InputMap.get_action_list("move_right"))
    save_settings()

General overview of User Interface (UI)

Making a basic UI in Godot is not very difficult as it uses the same concept of nodes that you add in your scene. However there is some subtleties that might be confusing at the beginning. If you are doing a HUD there is a good chance that you do not want to see it moving around or rotate with the camera. To avoid this, this is really easy, just add a CanvasLayer under your root node where you will put all your UI's nodes. From now one the UI will not follow the transformations applied to the camera.

The most tricky part of making a UI is to make it adaptable to many resolutions. Hopefully Godot comes with a special feature called Anchors. Each anchor represents a side of your UI element, left, right, top and bottom. You can choose what an anchor has to follow in case of resize:

  • Begin: the top and bottom anchors will follow the top boundary of the parent node. The left and right anchors will follow the left boundary of the parent node.
  • End: the top and bottom anchors will follow the bottom boundary of the parent node. The left and right anchors will follow the right boundary of the parent node.
  • Center: the top and bottom anchors will follow the vertical center boundary of the parent node. The left and right anchors will follow the horizontal center boundary of the parent node.

You still have to give the initial position, relative to the parent node top-left corner, and size for the default resolution (see project settings). What is going on behind the scene is very simple, Godot will calculate the margins for the initial values depending on the anchors setup. Then as the size of the parent node change, Godot will ensure the margins stay the same between the anchor and the selected boundary.

Signals

Signals are an implementation of the Observer Pattern. This a very convenient way to to make two objects interact together without any static reference to each other. Signals are important for user interaction for two reasons:

  1. This is how your UI elements interact with the rest of your code.
  2. It is an easy way to create feedback to the player.

To connect an UI element to your script you can use the Signals editor which comes with Godot. It will create a signal handling function in your existing script where you can implement logic on UI event:

func _on_play_pressed():
    emit_signal("next_screen", "game")

In the example above, you can see that you can even send another signal from a signal handler. In this case, I use a signal called next_screen to tell the game logic when a screen is done and it should switch to the next one. The code to create the signal is in /src/screens/abstract_screen.gd and it is as short as it can get:

signal next_screen(screen)

We just have to connect it in /src/game.gd to be able to implement the logic:

new_screen.connect("next_screen", self, "_load_screen")

I also made use of signals in other part of the game, like to detect the level failed or win state to give feedback to the player, code from /src/levels/abstract_level.gd:

signal failed()
signal victory()

which translate in /src/screens/game/screen_game.gd:

level.connect("failed", self, "_on_level_failed")
level.connect("victory", self, "_on_level_victory")

The main menu

Using all this techniques implementing the main menu is really trivial. The buttons simply emit the signal to change screen or quit the game.

# Contains the logic for the main menu screen
extends "../abstract_screen.gd"

func _on_play_pressed():
    emit_signal("next_screen", "game")

func _on_quit_pressed():
    get_tree().quit()

func _on_controls_pressed():
    emit_signal("next_screen", "controls")

The Controls sub-menu is little bit more tricky as it implement the key rebinding system:

# Contains the logic the controls screen
extends "../abstract_screen.gd"

var move_up_inputs
var move_down_inputs
var move_left_inputs
var move_right_inputs

var binding_popup

func _ready():
    move_up_inputs = find_node("move_up_inputs")
    move_down_inputs = find_node("move_down_inputs")
    move_left_inputs = find_node("move_left_inputs")
    move_right_inputs = find_node("move_right_inputs")
    binding_popup = find_node("binding_popup")
    binding_popup.set_focus_mode(Control.FOCUS_ALL)
    _load_inputs()

func _on_back_pressed():
    emit_signal("next_screen", "main_menu")
    
func _load_inputs():
    var fmt = "Key %s"
    
    move_up_inputs.clear()
    var inputs = UserSettings.get_keybindings("move_up")
    for input in inputs:
        move_up_inputs.add_button(fmt % UserSettings.convert_key_to_string(input.scancode))
        
    move_down_inputs.clear()
    inputs = UserSettings.get_keybindings("move_down")
    for input in inputs:
        move_down_inputs.add_button(fmt % UserSettings.convert_key_to_string(input.scancode))
        
    move_left_inputs.clear()
    inputs = UserSettings.get_keybindings("move_left")
    for input in inputs:
        move_left_inputs.add_button(fmt % UserSettings.convert_key_to_string(input.scancode))
        
    move_right_inputs.clear()
    inputs = UserSettings.get_keybindings("move_right")
    for input in inputs:
        move_right_inputs.add_button(fmt % UserSettings.convert_key_to_string(input.scancode))

func _on_move_up_inputs_button_selected( button_idx ):
    binding_popup.popup_binding("move_up", button_idx)

func _on_move_down_inputs_button_selected( button_idx ):
    binding_popup.popup_binding("move_down", button_idx)

func _on_move_left_inputs_button_selected( button_idx ):
    binding_popup.popup_binding("move_left", button_idx)

func _on_move_right_inputs_button_selected( button_idx ):
    binding_popup.popup_binding("move_right", button_idx)

func _on_cancel_pressed():
    binding_popup.set_hidden(true)

func _on_binding_popup_key_pressed( action, input, scancode ):
    UserSettings.replace_keybinding(action, input, scancode)
    _load_inputs()
    binding_popup.set_hidden(true)

This menu makes use of the HButtonArray UI element, one for each action. Using this node simplify adding and removing buttons, one for each input. When one of the button is pressed a popup appear. This popup makes also use of a signal to return which key has been pressed and should be used to replace the current binding:

# Contains the logic for the binding popup
extends PopupPanel

signal key_pressed(action, input, scancode)

var current_action
var current_input

func _ready():
    set_process_input(true)

func popup_binding(action, input):
    current_action = action
    current_input = input
    popup_centered()

func _input(event):
    if(is_visible() && event.type == InputEvent.KEY && event.pressed):
        emit_signal("key_pressed", current_action, current_input, event.scancode)

What is next?

In the next article we will speak about the player's character and the differents Godot features I used to implement it.

Written by Olivier on Monday October 31, 2016

« Creating a game with Godot Engine - Ep.3 - General considerations Creating a game with Godot Engine - Ep.5 - Player's character »

comments powered by Disqus