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
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
#
# (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
#
# (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.
#
# (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.