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.3 - General considerations

In this third article we will look into general considerations you have to take into account when starting a new project. We will start with a quick look at the project settings. Then, we will introduce the notion of singleton and how to use it to create a user settings system. Finally, we will see how to create an entry point. (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

When starting a new project, there is some preparatory work that you might want consider. The goal is to create a setup which will make your life easier later on. It is also good to know where some of the Godot features are.

Project settings

The first thing you might want to do after creating a project is to go in the Project Settings. You will find this dialog under the Scene menu. For the moment we will only look at the General tab. On the left, there is a list of sections, we will go through in a minute. On the right, you will find a list of properties. Each property line has 3 columns, the first is a checkbox which, when checked, indicates a non-default value. The second column is the property's name. The third is the property's value.

We will now go through the different sections to understand what they are all about, and look into some of the important properties. I will not explain every single properties, only the ones the are relevant for a basic 2D project:

  • Application: contains the general application settings:
    • name: this is the name of the project which will appear in different place like the Godot project list or the window title.
    • main_scene: this the entry point of the project, for more details, look below.
    • icon: this is the icon of the project.
  • Debug: contains the properties related to the debugging features of Godot.
  • Display: contains the properties related to how the project is displayed on the screen:
    • fullscreen: determines if the game is using the entire screen. During development it is better to stay in window mode (fullscreen off).
    • width and height: size of the game's window. I usually set it to 1280x720 for two reasons. First because if you are using a Full HD screen then the window is not too small or too big. Second because most of the screens are in 16/9 aspect ratio, and this size respect it. If you have a target size and/or aspect ratio I would advise to set this properties accordingly.
    • allow_hidpi: if you are like me and use a screen with very high resolution, you might want to activate this option so the game does not appear too small.
    • use_2d_pixel_snap: determines if the display will round position of rendered elements to the closest integer. If you are using physically based movement then you might want to activate this option. Because the position can change by not round number, it can cause image distortions which do not look nice, especially with pixel art.
  • Render: contains the properties related to how a frame is rendered:
    • default_clear_color: determines the color used to clear the game's display. I usually set it to black to have a background as neutral as possible.
  • Audio: contains the properties related to the audio output settings.
  • Physics: contains the properties related to the Godot physic engine behavior.
  • Core: contains the properties related to low-level Godot features.
  • Rasterizer.android: contains the properties related to the rasterizer behavior on Android.
  • Display.android: contains the properties related to the display behavior on Android.
  • Rasterizer.ios: contains the properties related to the rasterizer behavior on iOS.
  • Display.ios: contains the properties related to the display behavior on iOS.
  • Rasterizer: contains the properties related to the rasterizer behavior.
  • Physic 2d: contains the properties related to the Godot 2D physic engine behavior:
    • default_gravity_vector: control the gravity direction. If you are doing a top-down game, you should put this to (0, 0) to be able to use the physic engine, but you want to avoid objects "falling down".
  • Image loader: contains the properties related to the behavior of Godot when making textures from images:
    • filter: determines if the image will be smoothed when the scale is not 1:1. If you are doing a pixel art game, this should be disabled.
    • gen_mipmaps: determines if Godot should generate sub-images for different scales. If you are doing a pixel art game, this should be disabled.
  • Ssl: contains the properties related to the Godot secure connection feature. If you plan to make an online game and you want the connection to the server to be encrypted, this is where you have to start.
  • Locale: contains the properties related to the Godot localization feature.
  • Global: contains the properties to change the Godot global behavior on a project basis.
  • Editor: contains the properties to change the Godot editor behavior on a project basis.
  • Autoload: equivalent of the Autoload tab, more details below.

Singleton

Godot comes with an autoload feature which allows to create singleton. You can access this feature through the Autoload tab of the Project Settings dialog. Through the autoload you can make a script accessible in all other script without adding a specific node to the scene. To add an entry, you have to give a name with which you will access your script later on and a path to the script resource. You can then choose to make it a singleton or not. The difference between a normal autoloaded script and a singleton is, for the singleton, there will be only one instance of your script for the whole game. It means that you can keep a global state, and associated functions, easily accessible from every other scripts. At last, you can use the arrows to reorder the autoloads. It is important if you have autoloaded scripts which depend on others, you have to make sure the dependencies are loaded before.

User settings

As an example for a singleton, we can create a basic user settings functionality for our demo project. Here the code (explanations below):

# User settings singleton
extends Node

const USER_SETTINGS_PATH = "user://user_settings.cfg"

const KEY_BINDING_SECTION = "key_binding"

var _user_settings

func _ready():
    load_settings()
    if not _user_settings.has_section(KEY_BINDING_SECTION):
        _create_default_key_bindings()
    _load_key_bindings()

func load_settings():
    # We create an empty file if not present to avoid error while loading settings
    var file = File.new()
    if not file.file_exists(USER_SETTINGS_PATH):
        file.open(USER_SETTINGS_PATH, file.WRITE)
        file.close()
    
    _user_settings = ConfigFile.new()
    var err = _user_settings.load(USER_SETTINGS_PATH)
    if err != OK:
        print("[ERROR] Cannot load user settings")
    
func save_settings():
    var err = _user_settings.save(USER_SETTINGS_PATH)
    if err != OK:
        print("[ERROR] Cannot save user settings")
        
func get_keybindings(action):
    return _user_settings.get_value(KEY_BINDING_SECTION, action)
    
func replace_keybinding(action, input, scancode):
    var previous = InputMap.get_action_list(action)
    var event = InputEvent()
    event.type = InputEvent.KEY
    event.scancode = scancode
    previous[input] = event
    _user_settings.set_value(KEY_BINDING_SECTION, action, previous)
    save_settings()
    _load_key_bindings()
    
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()
        
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)
            
func convert_key_to_string(scancode):
    var keys = {
        KEY_A: "A",
        KEY_B: "B",
        KEY_C: "C",
        KEY_D: "D",
        KEY_E: "E",
        KEY_F: "F",
        KEY_G: "G",
        KEY_H: "H",
        KEY_I: "I",
        KEY_J: "J",
        KEY_K: "K",
        KEY_L: "L",
        KEY_M: "M",
        KEY_N: "N",
        KEY_O: "O",
        KEY_P: "P",
        KEY_Q: "Q",
        KEY_R: "R",
        KEY_S: "S",
        KEY_T: "T",
        KEY_U: "U",
        KEY_V: "V",
        KEY_W: "W",
        KEY_X: "X",
        KEY_Y: "Y",
        KEY_Z: "Z",
        KEY_0: "0",
        KEY_1: "1",
        KEY_2: "2",
        KEY_3: "3",
        KEY_4: "4",
        KEY_5: "5",
        KEY_6: "6",
        KEY_7: "7",
        KEY_8: "8",
        KEY_9: "9",
        KEY_LEFT     : "Left",
        KEY_RIGHT    : "Right",
        KEY_UP       : "Up",
        KEY_DOWN     : "Down",
        KEY_SPACE    : "Space",
        KEY_TAB      : "Tab",
        KEY_BACKTAB  : "Backtab",
        KEY_BACKSPACE: "Backspace",
        KEY_RETURN   : "Return",
        KEY_ENTER    : "Enter",
        KEY_INSERT   : "Insert",
        KEY_DELETE   : "Delete",
        KEY_SHIFT    : "Shift",
        KEY_CONTROL  : "Ctrl",
        KEY_META     : "Meta",
        KEY_ALT      : "Alt"
    }
    if keys.has(scancode):
        return keys[scancode]
    else:
        return ""

In this article we will only cover the general aspect of this singleton, details about the InputMap and key rebinding will be given in the next article.

Except if you know what you are doing, a singleton should extends the Node class. By looking quickly at the code you may have realized that some variables and functions start with an underscore ('_') and some other do not. This is a very simple way to indicate what you want to expose to other scripts or not. Functions and variables starting with an underscore can only be access from inside the singleton script, they are private.

As a good practice when you write a script, you should group elements of the same nature together. You should also try to keep the same order for those groups in all your scripts. Here an example of grouping and order that I use:

  • Constants: in the UserSettings code I use constants to declare the path to the user settings file and the name of sections. It is a good practice to use constants instead of repeating the same value all over your script, for two reasons. First, you can give this value a meaningful name to help you remember what this value is all about. Second, in case you have to change the value, you only have to change it in one location.
  • Variables, public then private: variables are used to store the state of your script. In the UserSettings code I used a variable to store the current settings, but as I do not want to be access directly, it is private.
  • The _ready function: this is a special function automatically called by Godot when the script has been loaded. I like to put it in front of all other functions because this where all the initialization code takes place.
  • Functions, public then private: this is where you implement the logic of your script. You want to expose as public the functions that you want to be used in other scripts. You want to hide as private the functions that are internal to your script. It is a good practice to use private functions to avoid getting very long public functions and repeating code.
  • Some exceptions: you can allow yourself to override those rules if it makes more sens to you. In the UserSettings code you may have realize that the convert_key_to_string function is misplaced. In reality it is not, I decided to put it at the end for two reasons. First, this is utility function which is not part of the core features of the singleton. Second, this function is a quite long function which would disturb code reading if placed in the middle.

My implementation of  a user settings is based around the ConfigFile class of Godot. This a very convenient class that you can use to easily store key/value pairs into a human readable file. It also provides a section feature to help you organize nicely your configuration file. I also make use of another very nice feature of Godot. If you look at the path of the file, it starts with user:// instead of res://. This prefix is a shortcut to access a writable folder under the user personal directory. The actual path depends on the OS the game is executed on. Every time you will need to to write a file (save file, settings, ...) on the player computer, you should use it.

One thing important to keep in mind is that when the player start the game for the first time, the file is not present. This is why I use the function File.file_exists to test it and then simply create an empty file if not present. Therefore I can load the settings by calling ConfigFile.load without getting an error.

You can now access, modify and save the settings by calling the functions given by ConfigFile on the _user_settings variable. As you cannot access those functions from other scripts, _user_settings is private, you will have to create convenient functions to access specific settings, like I did with get_keybindings and replace_keybinding.

Project entry point

If you are making any kind of serious project you will have multiple scenes to display. Therefore, it is a good idea to create an entry point scene which will manage the scene switching logic. To do so, create a scene at the root of your Godot project called game.tscn, the name is not important, just choose one which makes sens to you. Add a node of type Node in the scene and call it root. Then attach a script to this node and called it game.gd.

# Contains the game logic
extends Node

var current_screen
var fps

var screens = {
        "main_menu": preload("res://screens/main_menu/screen_main_menu.tscn"),
        "game": preload("res://screens/game/screen_game.tscn"),
        "controls": preload("res://screens/controls/screen_controls.tscn"),
    }
    
func _ready():
    set_process_input(true)
    set_process(true)
    current_screen = find_node("screen")
    fps = find_node("fps")
    _load_screen("main_menu")
    
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())
        
func _process(delta):
    fps.set_text("FPS: %d" % Performance.get_monitor(Performance.TIME_FPS))
        
func _load_screen(name):
    if name in screens:
        var old_screen = null
        if current_screen.get_child_count() > 0:
            old_screen = current_screen.get_child(0)
        if old_screen != null:
            current_screen.remove_child(old_screen)

        var new_screen = screens[name].instance()
        new_screen.connect("next_screen", self, "_load_screen")
        current_screen.add_child(new_screen)
    else:
        print("[ERROR] Cannot load screen: ", name)
        

To be able to switch between scenes, the first thing is to add a node of type Node2D called screen as a child of root. Then, as you will to access it from the script, you have to create a variable in your script, you can call it current_screen. In the _ready function you can use Node.find_node, using "screen" as parameter, to store the reference to the node in the variable. Another good practice is to preload all your scenes in a dictionary inside a variable called screens. It will make your life easier when switching.

The actual switching code is the _load_screen function. The first thing is to check if the asked scene exists. Then we remove the previous scene if there is any. Creating a new scene is done by calling PackedScene.instance on the corresponding preloaded scene. We connect _load_screen function to the next_screen signal of the newly created scene, I will explain why in the next article. To display the scene we just add it as a child of the current_screen.

We also call _load_screen in the _ready function to load the screen we want to be displayed first. In this case it is the main menu, which will be discussed in the next article.

What is next?

In the next article we will speak about input map, key rebinding, user interfaces, signals, main menu.

Written by Olivier on Saturday October 22, 2016

« Creating a game with Godot Engine - Ep.2 - Project organization Creating a game with Godot Engine - Ep.4 - User Interaction »

comments powered by Disqus