|
| 1 | +--- |
| 2 | +layout: post-with-example |
| 3 | +title: Editor scripting improvements in the upcoming 1.10.4 version |
| 4 | +excerpt: Defold editor now supports editing scenes using editor scripts! |
| 5 | +author: Vlad Protsenko |
| 6 | +tags: ["editor", "scripting"] |
| 7 | +--- |
| 8 | + |
| 9 | +Defold editor now supports editing scenes using editor scripts! The new capabilities were released incrementally over the last couple of months, and only ended up in release notes as a series of small entries like "Now you can edit tilemaps using editor scripts". But now that most of the work is done, we'd like to share an overview of the whole thing. We will do it by writing an editor script — together — that assembles the following scene: |
| 10 | + |
| 11 | +<div id="app-container" class="canvas-app-container" style="display:none"> |
| 12 | + <div id="canvas-container" class="canvas-app-canvas-container"></div> |
| 13 | + <div class="buttons-background"></div> |
| 14 | +</div> |
| 15 | +<!-- the canvas is intentionally moved outside of the app-container to |
| 16 | +not be affected by any scaling options set in game.project --> |
| 17 | +<canvas id="canvas" class="canvas-app-canvas" tabindex="1" width="680" height="680"></canvas> |
| 18 | +<script id='engine-loader' type='text/javascript' src="/postsdata/2025-07-11-editor-scripting-update/dmloader.js"></script> |
| 19 | +<script id='engine-start' type='text/javascript'> |
| 20 | + EngineLoader.load("canvas", "/postsdata/2025-07-11-editor-scripting-update/EditorScriptingDemo"); |
| 21 | +</script> |
| 22 | + |
| 23 | +Tip: You can use touch and drag to rotate the scene. If you are reading this on a computer, you can even use WASD controls! Don't tell the mobile users what Q and E do! |
| 24 | + |
| 25 | +# It's just files, right? |
| 26 | + |
| 27 | +Since most defold assets are edited as textual protobuf files, and the protobuf definitions are published with every release, it was always possible to edit them — they are just files, after all. First, let's start with an editor script that creates "just files". Then, we will discuss the problems with files, and when it makes sense to use another approach. In this post, we will implement a realistic use case that you might want to automate when working with Defold: importing asset packs. |
| 28 | + |
| 29 | +Let's say you want to use Kenney's beautiful [Castle Kit](https://kenney.nl/assets/castle-kit) for a fantasy city builder prototype. You download the pack and extract the GLB models to the project folder. Now, to use the assets in a scene, you need to create game objects for the models. Let's try creating a single one manually for now. We will also use Dragosha's wonderful [light-and-shadows](https://github.com/Dragosha/defold-light-and-shadows) pack for shading, so we will need to set up some custom materials on a model. |
| 30 | + |
| 31 | +Here is how a manually-made game object looks: |
| 32 | + |
| 33 | + |
| 34 | +To create it, we need to make a new game object with an embedded model component. Then, we set the following properties on a model: |
| 35 | +- `Mesh` — points to the GLB file from Kenney's asset pack |
| 36 | +- `colormap` — material that casts shadows, from Dragosha's pack |
| 37 | +- `tex0` — image to colorize the model |
| 38 | + |
| 39 | +It would be time-consuming to repeat these steps for every model, so let's write a simple editor script to automate it. |
| 40 | + |
| 41 | +## Editor Script Part 1: a Command |
| 42 | + |
| 43 | +Let's start with creating an `.editor_script` file and removing all the boilerplate: |
| 44 | +```lua |
| 45 | +local M = {} |
| 46 | + |
| 47 | +function M.get_commands() |
| 48 | + return {} |
| 49 | +end |
| 50 | + |
| 51 | +return M |
| 52 | +``` |
| 53 | + |
| 54 | +Now, let's modify `M.get_commands()` to return a command that prints a message when executed. Start small. |
| 55 | +```lua |
| 56 | +return { |
| 57 | + editor.command({ |
| 58 | + label = "Generate models", |
| 59 | + locations = {"Edit"}, |
| 60 | + id = "app.generate-models", |
| 61 | + run = function() |
| 62 | + print("Lets go!") |
| 63 | + end |
| 64 | + }) |
| 65 | +} |
| 66 | +``` |
| 67 | +Tip: if you create a command with an `id`, you can then open **File → Preferences** and assign a shortcut to this command in a **Keymap** tab. This comes in handy when iterating on editor scripts. I picked <kbd>Ctrl+G</kbd>, where G is a mnemonic for Generate. |
| 68 | + |
| 69 | +Now, if you reload editor scripts (**Project → Reload Editor Scripts** or <kbd>Cmd+Shift+R</kbd>) and run the command (either using **Edit → Generate Models**, or a custom shortcut), you will see `Lets go!` printed in the console — we are ready to proceed. |
| 70 | + |
| 71 | +## Editor Script Part 2: Generating Game Objects |
| 72 | + |
| 73 | +Let's have a look at the game object file as a text: |
| 74 | +``` |
| 75 | +embedded_components { |
| 76 | + id: "model" |
| 77 | + type: "model" |
| 78 | + data: "mesh: \"/assets/GLB format/siege-tower.glb\"\n" |
| 79 | + "name: \"{{NAME}}\"\n" |
| 80 | + "materials {\n" |
| 81 | + " name: \"colormap\"\n" |
| 82 | + " material: \"/light_and_shadows/materials/model/model_world.material\"\n" |
| 83 | + " textures {\n" |
| 84 | + " sampler: \"tex0\"\n" |
| 85 | + " texture: \"/assets/GLB format/Textures/colormap.png\"\n" |
| 86 | + " }\n" |
| 87 | + "}\n" |
| 88 | + "" |
| 89 | +} |
| 90 | +``` |
| 91 | + |
| 92 | +Source files use a [protobuf](https://protobuf.dev/) text format. Our embedded model is embedded as an escaped protobuf text. This happens because protobuf does not support generic message types, so we have to emulate them. Not very convenient, but we still prefer to use the format because protobuf resources can be compiled to very small binaries, and we care about small bundle sizes. Anyway, when creating files, we will only need to replace the `siege-tower` part with a model name, so let's make it a template: |
| 93 | + |
| 94 | +```lua |
| 95 | +local template = [[embedded_components { |
| 96 | + id: "model" |
| 97 | + type: "model" |
| 98 | + data: "mesh: \"/assets/GLB format/%s.glb\"\n" |
| 99 | + "name: \"{{NAME}}\"\n" |
| 100 | + "materials {\n" |
| 101 | + " name: \"colormap\"\n" |
| 102 | + " material: \"/light_and_shadows/materials/model/model_world.material\"\n" |
| 103 | + " textures {\n" |
| 104 | + " sampler: \"tex0\"\n" |
| 105 | + " texture: \"/assets/GLB format/Textures/colormap.png\"\n" |
| 106 | + " }\n" |
| 107 | + "}\n" |
| 108 | + "" |
| 109 | +} |
| 110 | +]] |
| 111 | +``` |
| 112 | + |
| 113 | +As you can see, the name is replaced with `%s` — we will use `template:format(name)` to create `.go` files. |
| 114 | + |
| 115 | +We will use the new [`editor.create_resources()`](https://defold.com/ref/alpha/editor-lua/#editor.create_resources:resources) function to create all the files at once — it is significantly more performant than using Lua's utilities for writing files. To create multiple files with custom content, we will need to call it with a list of tuples: file names and contents. For example, it might look like that: |
| 116 | +```lua |
| 117 | +editor.create_resources({ |
| 118 | + {"/assets/models/get/siege-tower.go", template:format("siege-tower")} |
| 119 | +}) |
| 120 | +``` |
| 121 | +Now, let's change the `run` function to create game objects for all the models: |
| 122 | +```lua |
| 123 | +-- select and clear the directory for generated assets |
| 124 | +local root_dir = "/assets/models/gen" |
| 125 | +editor.delete_directory(root_dir) |
| 126 | +-- list all assets from the asset pack |
| 127 | +local assets = editor.get("/assets/GLB format", "children") |
| 128 | +-- this is the argument to `editor.create_resources()`: |
| 129 | +local resources = {} |
| 130 | +for i = 1, #assets do |
| 131 | + local glb_path = assets[i] |
| 132 | + local base_name = glb_path:match("([^/]+)%.glb$") |
| 133 | + if base_name then |
| 134 | + local go = root_dir .. "/" .. base_name .. ".go" |
| 135 | + resources[#resources+1] = {go, template:format(base_name)} |
| 136 | + end |
| 137 | +end |
| 138 | +editor.create_resources(resources) |
| 139 | +``` |
| 140 | +Try reloading the editor scripts and executing the command again: it will create a game object for every model — these can be used for making scenes! Nice! |
| 141 | + |
| 142 | + |
| 143 | + |
| 144 | +# When files are not enough |
| 145 | + |
| 146 | +So far, we have a simple file-based API to import assets, nothing fancy here. But using file formats is much harder when you want to create complex scenes. For example, collections are trees of game objects, but the file format of a collection is a flat list, where game objects refer to their children by ids. It's inconvenient to take a tree and then convert it to such a flat list. Additionally, there is an extra complication: since a file format uses ids to refer to children, there is a requirement — but only in the file format — that every id must be specified, and unique. But when creating a collection, in a lot of cases you don't care about ids of game objects that exist only to place something in a scene. So. |
| 147 | + |
| 148 | +The problem is this — sometimes, you don't want to think in terms of file formats. When it comes to scenes, you want to edit them as scenes. And this is exactly what we've been working on for the last 2 months. You can edit collections and GUIs as trees, and the editor will handle file formats and generate ids for you. You can edit tilemaps as 2D grids. Et cetera. |
| 149 | + |
| 150 | +Now, let's compose a scene! |
| 151 | + |
| 152 | +## Editor script part 3: making a collection |
| 153 | + |
| 154 | +We are going to place the created game objects in a scene to preview all of them together. Additionally, let's show a label with the name of the asset alongside every model. This is what we are going to do in the final part of the editor script: |
| 155 | + |
| 156 | + |
| 157 | + |
| 158 | +First, let's create an empty `/assets/all.collection`. Then, we will edit its children — the `children` property — using the editor's [built-in transactional editing API](https://defold.com/manuals/editor-scripts/#editing-collections). This API is very useful for making commands that group multiple edits in a single undoable step, though it doesn't matter much in our case. If simplified, the code would look something like this: |
| 159 | +```lua |
| 160 | +local coll = "/assets/models/all.collection" |
| 161 | +editor.transact({ |
| 162 | + editor.tx.add(coll, "children", { |
| 163 | + -- add referenced game object |
| 164 | + type = "go-reference", |
| 165 | + path = "/assets/models/gen/siege-tower.go", |
| 166 | + children = { |
| 167 | + -- add embedded game object with a label |
| 168 | + { |
| 169 | + type = "go", |
| 170 | + components = { |
| 171 | + {type = "label", text = "siege-tower"} |
| 172 | + } |
| 173 | + } |
| 174 | + } |
| 175 | + }) |
| 176 | +}) |
| 177 | +``` |
| 178 | +Now let's implement the real thing. We already have a `resources` array with tuples of game object paths + their contents. Let's use it to edit the scene. We need to add the following code at the bottom of the `run` handler: |
| 179 | +```lua |
| 180 | +local coll = "/assets/models/all.collection" |
| 181 | +-- txs array, start by clearing the collections, so we don't add |
| 182 | +-- too many items when re-generating the assets |
| 183 | +local edit_txs = { editor.tx.clear(coll, "children") } |
| 184 | + |
| 185 | +local row = math.floor(math.sqrt(#resources)) |
| 186 | +local half = math.floor(row / 2) |
| 187 | +for i = 1, #resources do |
| 188 | + local go = resources[i][1] |
| 189 | + local x = (i - 1) % row |
| 190 | + local z = math.floor(i/row) |
| 191 | + edit_txs[#edit_txs+1] = editor.tx.add(coll, "children", { |
| 192 | + type = "go-reference", |
| 193 | + path = go, |
| 194 | + -- position all objects around 0,0 |
| 195 | + position = {- (x - half) * 1.5, 0, - (z - half) * 1.5}, |
| 196 | + children = { |
| 197 | + { |
| 198 | + -- no id necessary, it's auto-generted when omitted |
| 199 | + type = "go", |
| 200 | + scale = {0.007, 0.007, 0.007}, |
| 201 | + position = {0, 0.2, 0.7}, |
| 202 | + components = { |
| 203 | + { |
| 204 | + type = "label", |
| 205 | + text = go:match("([^/]+)%.go$"), |
| 206 | + font = "/main/font.font", |
| 207 | + material = "/builtins/fonts/label.material" |
| 208 | + } |
| 209 | + } |
| 210 | + } |
| 211 | + } |
| 212 | + }) |
| 213 | +end |
| 214 | +editor.transact(edit_txs) |
| 215 | +-- we don't care about undo here, might as well save: |
| 216 | +editor.save() |
| 217 | +``` |
| 218 | + |
| 219 | +And voila, that's how it's done! Reload editor scripts, re-run the command, and you'll have a collection with every item. |
| 220 | + |
| 221 | +# P.S. |
| 222 | + |
| 223 | +If you want to learn more, check out the [editor scripting docs](https://defold.com/manuals/editor-scripts/). Here is [the code for the demo](https://github.com/vlaaad/editor-scripting-assets), and here is [the full editor script source](https://github.com/vlaaad/editor-scripting-assets/blob/main/gen_models.editor_script). Note that collection editing and `editor.create_resources()` are only available in Defold version 1.10.4, which, at the time of writing, has not been released yet. |
| 224 | + |
| 225 | +We'd be happy to talk more about other, already released editing capabilities, such as [tilemaps](https://defold.com/manuals/editor-scripts/#editing-tilemaps), but this post is already getting too long. Some other time, maybe? |
| 226 | + |
| 227 | +Meanwhile, happy Defolding! |
| 228 | + |
| 229 | + |
0 commit comments