LDtk Projects in Godot - Part 1: Setup

Setting up a Godot Import Plugin

Source code corresponding to this particular article can be found here: https://gitlab.com/letsmakegames/modules/godot/ldtk

For our game Moonlight Mountain I decided to try Godot for the first time. While it has some built in Level Editing tools, I decided to go with supporting LDtk projects instead. At the time of writing, LDtk reached it's first 1.0 milestone just 6 days ago! We were a big fan of the workflow, and in this series of articles, I want to talk a little about how we set it up, and how I went about building a custom importer. NOTE I'm new to both Godot and LDtk so this is more devlog than tutorial. Hope it helps!

Official Documentation

In this article I want to briefly cover setting up a plugin / editor addon for Godot. Here are some relevant Godot documentation links if you want to take a look:

Why Build another LDtk Importer?

This is a great question. There are several LDtk plugins readily available, so when we are building a weekend game jam, why did I choose to write a new one from scratch* (or, well, from copying parts of other implementations) rather than just one one ready made?

I wrote a long essay here, and maybe it'd be an interesting article on it's own, but the short answer is - a general purpose level loader that tries to account for all possible games either needs to be incredibly complex, or make a bunch of assumptions about the game being made.

Complexity causes friction and increases the surface area for bugs and user errors.

Assumptions restrict the game design and possible approaches.

I probably could have worked around either, but after looking at the code (which is pretty straight forward), I decided it would be faster to write a game specific importer (which I think the author of LDtk intended honestly) rather than try and reuse an existing one and absorb all that additional baggage.

This is not a choice that should be generalized. Generally the decision about what should be built reusable and one is project specific is a pretty personal choice. It's not really about right or wrong, but something you develop a personal feel for over the years, kinda like how much butter you like on your toast, or what temperature you prefer your office at. For me, there was so much game logic embedded into the level format, that I felt it was best to do something specific for this game. I think there are bits that would make sense in a more general purpose context, of course, and the purpose of writing this devlog is to maybe help identify those pieces.

Plugin.cfg

We'll be building a special case of plugin - an EditorImportPlugin which will be called whenever Godot detects an LDtk file in our project, or whenever that file changes.

The first file necessary is a Plugins.cfg file, located in your projects addons folder. I've created a folder lmg-ldtk where all the files will go

addons/lmg-ldtk/plugin.cfg
config_version=1

[plugin]
name="LetsMakeGames LDtk Integration"
description="Yet Another LDtk Integration for Godot"
version="0.1"
author="https://letsmake.games/"
script="ldtkPlugin.gd"

This is the file that Godot will use to detect your plugin, and make it available in the Plugins tab of the Project Settings window

Here you can see three plugins. WAT a great unit testing framework, and my two LDtk Integration addons

EditorPlugin

The config file contains a name, description, version, and author. Most importantly it references the entry point for our addon. Here I've specified an ldtkPlugin.gd script. We'll add that next

addons/lmg-ldtk/ldtkPlugin.gd
#
# (c) LetsMakeGames 2022
# http://www.letsmake.games
#

tool
extends EditorPlugin

var importPlugin = null

#
# public api ##################################################################
#

func get_plugin_name():
    return "lmg-LDtk"

#
# -----------------------------------------------------------------------------
#

func _enter_tree():
    importPlugin = preload("ldtkLoader.gd").new()
    add_import_plugin(importPlugin)


#
# -----------------------------------------------------------------------------
#

func _exit_tree():
    remove_import_plugin(importPlugin)
    importPlugin = null

This plugin has 3 methods. An accessor for getting the plugin name, followed by callbacks for _enter_tree() and _exit_tree(). These are specific lifecycle callbacks that correspond to our plugin being enabled and disabled in the project.

We use these hooks to instantiate (or free) a singleton of another class a special plugin that is used by Godot whenever importing files. We create an ldtkLoader.gd plugin, which will tell Godot how to handle ldtk project files. Lets take a look at that next.

EditorImportPlugin

addons/lmg-ldtk/ldtkLoader.gd
#
# (c) LetsMakeGames 2022
# http://www.letsmake.games
#

tool
extends EditorImportPlugin

#
# fields ######################################################################
#

var LDtkProject = preload("ldtkProject.gd").new()

#
# #############################################################################
#

func get_importer_name():
    return "lmg.LDtk.int"

#
# -----------------------------------------------------------------------------
#

func get_visible_name():
    return "LetsMakeGames LDtk Integration"

#
# -----------------------------------------------------------------------------
#

func get_priority():
    return 1

#
# -----------------------------------------------------------------------------
#

func get_resource_type():
    return "PackedScene"

#
# -----------------------------------------------------------------------------
#

func get_recognized_extensions():
    return ["ldtk"]

#
# -----------------------------------------------------------------------------
#

func get_save_extension():
    return "tscn"

#
# -----------------------------------------------------------------------------
#

func get_import_options(preset):
    return [
    ]

#
# -----------------------------------------------------------------------------
#

func get_option_visibility(option, options):
    return true

#
# -----------------------------------------------------------------------------
#

func get_preset_count():
    return 1

#
# -----------------------------------------------------------------------------
#

func get_preset_name(preset):
    return "Default"

#
# -----------------------------------------------------------------------------
#

func import(sourceFile, savePath, options, platformVariants, genFiles):

    #
    # load ldtk project
    #
    var proj = LDtkProject.load_project(sourceFile)

    #
    # create result
    #
    var scene = PackedScene.new()

    var filename = "%s.%s" % [savePath, get_save_extension()]
    return ResourceSaver.save(filename, scene)

Editor import plugins have a well specified api that needs to be implemented as described by the official documentation but the key method for us here is the import function

You'll notice we reference yet another file, this one designed to encapsulate our raw LDtk operations (like loading the json file, and managing specific segments) Right now, we just use it to load the project file, which is easy enough with gdscripts built in JSON parsing API.

The last portion of the import method doesn't do much right now, but we will use it later to actually generate game assets for Godot.

Our output format is a scene file, specifically a packed scene file, which allows us to add as many nodes and assets as we need in order to fully instantiate a godot based LDtk project. For example, we will load several tilesets (which describe tile tiles used) and tilemaps (which are arrangement of those tiles) as well as other useful data.

At this point we are not generating any Godot nodes yet, but when we do they will end up in the PackedScene we are creating here.

We then use the ResourceSaver helper object to save this scene.

How Godot Handles Imported Files

When a file is imported into godot a few artifacts are generated. First, a .import file is created for the asset. For example icon.png gets an icon.png.import file. This is a TOML file that contains the import settings for the file. Additionally, a .import directory is created which contains generated artifacts of your import.

In our example this matters because we will be adding an ldtk file to our project (a json file). Our importer will process this json file, and generate a scene. From the users perspective, they will be interacting with the ldtk file. They can drag it into the heirarchy to instantiate the map, open it, inspect it, and even derive trees or new scenes from it. But on disk, it's still just a json file. Inside Godot, however, you will be interacting with a proxy file taht is actually a tscn or scene file. Specifically a PackedScene file that we return from the import function on the EditorImportPlugin.

LDtkProject

The last script we will look at is the script file where we will be putting all our raw LDtk methods in. The job of this collection of functions will be to aid in the parsing and generation of godot Nodes based on the information stored in the LDtk file.

Right now all we need to implement is the load_project method, which simply opens the asset file, and parses it, returning the resulting json object.

addons/lmg-ldtk/ldtkProject.gd
#
# (c) LetsMakeGames 2022
# http://www.letsmake.games
#

tool
extends Reference

#
# public api ##################################################################
#

func load_project(filepath):
    if not filepath is String:
        return null

    var jsonFile = File.new()
    jsonFile.open(filepath, File.READ)
    var json = JSON.parse(jsonFile.get_as_text()).result
    jsonFile.close()

    json['workingDir'] = filepath.get_base_dir()
    return json

The only extra information is us adding the working directory (or the directory where the LDtk file is stored) to the resulting json. This is helpful later, since the LDtk file references other files (like textures) relative to itself. This way our scripts can populate absolute paths to dependant assets.

Conclusion

Right now, this script will in fact import an LDtk file, but the result is just an empty scene. In the next article, we will take a look at parsing the TileSet portion of the json file, and create Godot tilesets based on the information within.