Editing the Godot Editor
23 Jun 2024This article will cover my experience playing with the Godot editor’s source code, and encapsulate practical examples of the lessons I try and teach juniors that are navigating a new code-base for the first time.
Preamble
If you want to get to the thick of things, skip this section. (If you’re anything like me, the first 10-15% of every youtube video or blog post being irrelevant to the overall content drives you nuts, so I’m trying to help you out!)
At the current stage of my career, mentoring has been one of my most important responsibilities. I frequently find myself helping juniors cultivate the techniques they’ll need to navigate a large, foreign code-base (though the code bases we put them on are hardly “large” in an absolute sense). Yet much of my recent work touches the same handful of repos, many of which are at least partly my brainchild. I couldn’t help but realize that it had been some years since I directly worked on a large, foreign code-base…
I pondered this on the way to my summer vacation, where I would be meeting up with my siblings, nieces, and nephews for the first time in half a year or so. A particular children’s exhibit we were visiting featured a Space-Invaders-like game, an my nephews penchant for crushing any high score I set inspired me to finally try out RayLib, and compare it with Godot 4.2.2.
My earnest interest in making games began when I was around 12 years old, and my sister bought me RPG Maker XP. While comfortable with a computer, I had no idea how programming languages worked, so I was resigned to tweaking existing Ruby scripts and programming logic through GUIs. Despite the tedium, I absolutely loved making tile maps:
The Mission
The most important tool in my tool-belt was the ellipse brush. After a few clicks, it was virtually impossible to not create interesting looking islands:
This was my most desired missing feature when previously using Godot. To my dismay, despite all of the excellent tile map additions they had made in the past couple of years, an ellipse tool was still missing. And as such, my adventure begins…
From the Top Down
The great part about editing an application you have used before is that you have existing keywords and concepts you can map from functionality back to source code. For instance, looking at the “TileMap” editor, I can see there is a “Rect” button:
These make for easy keyword searches. Looking at the godot repo, I see the following directories:
|-- .github/
|-- core/
|-- doc/
|-- drivers/
|-- editor/
|-- main/
|-- misc/
|-- modules/
|-- platform/
|-- scene/
|-- servers/
|-- tests/
`-- thirdparty/
Clearly, I am working on the editor, so editor
seems like a good bet for where
to search. The next directory down does not contain any files containing the
word “tile”, so my search continues:
|-- debugger/
|-- export/
|-- gui/
|-- icons/
|-- import/
|-- plugins/
|-- project_manager/
|-- themes/
`-- translations/
I know Godot supports plugins, often applications that support plugins implement
core functionality in terms of plugins (QtCreator immediately coming to mind),
so I have a lucky first guess and check plugins/
, which contains a tiles/
sub-directory. I open tile_map_editor.cpp/.h
and I’m off to the races!
I want to place my “Ellipse” brush next to the “Rect” brush, so I simply search for “rect” in the file, and vuala, the first text that greets me in the header file is:
Button *rect_tool_button = nullptr;
At this point, I feel confident that I’m at least on the right track, so now I
just need to see if I can build the code. Thankfully, the godot project has a
handy Compiling From Source
link directly in its README
file. Using the magic of “Reading the Friendly
Manual,” I have Scons successfully building an x64 Debug project via Visual
Studio 2022*:
$ scons platform=windows vsproj=yes dev_build=yes
The docs even tell me I can build from within Visual Studio in the future. Neat!
*As a NeoVim snob, I am contractually obligated to tell you that I usually use neovim.
Adding a Button
All I do is copy and edit the line:
Button *rect_tool_button = nullptr;
Button *ellipse_tool_button = nullptr;
Next comes a little bit of boilerplate. I just look for where rect_tool_button
is used and make a version using ellipse_tool_button
. Here’s an interesting
one: godot looks up an icon by name:
source_sort_button->set_icon(tiles_bottom_panel->get_editor_theme_icon(SNAME("Sort")));
select_tool_button->set_icon(tiles_bottom_panel->get_editor_theme_icon(SNAME("ToolSelect")));
paint_tool_button->set_icon(tiles_bottom_panel->get_editor_theme_icon(SNAME("Edit")));
line_tool_button->set_icon(tiles_bottom_panel->get_editor_theme_icon(SNAME("Line")));
rect_tool_button->set_icon(tiles_bottom_panel->get_editor_theme_icon(SNAME("Rectangle")));
bucket_tool_button->set_icon(tiles_bottom_panel->get_editor_theme_icon(SNAME("Bucket")));
Surely there’s an existing icon I can borrow. A lot of these names look pretty generic, but “Bucket” seems specific and related enough to narrow down a search:
$ git ls-files '*Bucket*'
editor/icons/Bucket.svg
One match! Nice and easy! I simply look in that directory for an image name “Elipse”, then “Circle”, and settle on “CircleShape2D”:
ellipse_tool_button->set_icon(tiles_bottom_panel->get_editor_theme_icon(SNAME("CircleShape2D")));
Compile (don’t forget to select the x64
configuration), debug, and I see some
success:
The Button Should Do Stuff
To start, I just want the button to do something. Duplicating the functionality of the Rect button seems like it should be pretty easy, so let’s start with that. This is a good time to illustrate a point:
1. When in Rome, do as the Romans do. In other words, try to make your addition look as similar to the existing code as you can.
bool TileMapEditorTilesPlugin::forward_canvas_gui_input(const Ref<InputEvent> &p_event) {
// ...Omitted for brevity...
} else if (tool_buttons_group->get_pressed_button() == rect_tool_button || (tool_buttons_group->get_pressed_button() == paint_tool_button && Input::get_singleton()->is_key_pressed(Key::SHIFT) && Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL))) {
drag_type = DRAG_TYPE_RECT;
drag_start_mouse_pos = mpos;
drag_modified.clear();
} else if (tool_buttons_group->get_pressed_button() == bucket_tool_button) {
// ...Omitted for brevity...
I see that we set a DRAG_TYPE_RECT
. Do I understand what any of this is
really doing? No, not at all. However, I will probably want a
DRAG_TYPE_ELLIPSE
eventually, so I’ll add one. This is a natural time to
discuss the next point:
2. Use the code-base’s preferred tools until when starting out. As you gain more experience and better understand tooling in general, you can break this rule*. However, to get up and running quickly, I want to know auto-complete and debugging will work out of the box.
Turns out, there are two separate enums named DragType
. I updated the wrong
one at first by mistake. Rather than waiting on the re-compile, my editor
immediately told me there was a problem, which allowed me to fix my mistake
quickly while it was fresh in my mind.
*Really, there are exceptions to every rule, but you should understand the rules before breaking them.
Take a moment to notice the code I copied. The else if
statement is much
shorter than the rect_tool_button
equivalent.
} else if (tool_buttons_group->get_pressed_button() == ellipse_tool_button) {
The rect_tool_button
uses the modifier keys. We don’t want to hijack this
behavior!
Input::get_singleton()->is_key_pressed(Key::SHIFT) &&
Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)))
3. You can’t know what every line of code does before making a change. However, you should do some basic due diligence. At a bare minimum, you should be reading the lines you are copying and modifying to try and gain a baseline understanding for how they work.
We continue searching for rect
and updating with an ellipse
version. This
eventually leads us to the _draw_rect
function! My plan was to just duplicate
the existing functionality, so I’ll do so in _draw_ellipse
. And…
Rats! What does this bug suggest? Well, I copied some parts properly, but must
have missed some others. The hunt continues… Turns out I just forgot to search
for DRAG_TYPE_RECT
in the void TileMapEditorTilesPlugin::_stop_dragging()
function. Easy fix.
Who Ordered An Ellipse?
Now we need to edit our _draw_ellipse
function to behave as advertised. This
is going to be the hard part. We need to understand the draw rect function:
HashMap<Vector2i, TileMapCell> TileMapEditorTilesPlugin::_draw_ellipse(Vector2i p_start_cell, Vector2i p_end_cell, bool p_erase) {
TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id));
if (!tile_map) {
return HashMap<Vector2i, TileMapCell>();
}
Ref<TileSet> tile_set = tile_map->get_tileset();
if (!tile_set.is_valid()) {
return HashMap<Vector2i, TileMapCell>();
}
// Create the rect to draw.
Rect2i rect = Rect2i(p_start_cell, p_end_cell - p_start_cell).abs();
rect.size += Vector2i(1, 1);
// Get or create the pattern.
Ref<TileMapPattern> erase_pattern;
erase_pattern.instantiate();
erase_pattern->set_cell(Vector2i(0, 0), TileSet::INVALID_SOURCE, TileSetSource::INVALID_ATLAS_COORDS, TileSetSource::INVALID_TILE_ALTERNATIVE);
Ref<TileMapPattern> pattern = p_erase ? erase_pattern : selection_pattern;
HashMap<Vector2i, TileMapCell> err_output;
ERR_FAIL_COND_V(pattern->is_empty(), err_output);
Most of this code is common between _draw_rect
and _draw_bucket_fill
, so for
now we’ll assume they’re not relevant (sans the Rect2i
lines).
// Compute the offset to align things to the bottom or right.
bool aligned_right = p_end_cell.x < p_start_cell.x;
bool valigned_bottom = p_end_cell.y < p_start_cell.y;
Vector2i offset = Vector2i(aligned_right ? -(pattern->get_size().x - (rect.get_size().x % pattern->get_size().x)) : 0, valigned_bottom ? -(pattern->get_size().y - (rect.get_size().y % pattern->get_size().y)) : 0);
HashMap<Vector2i, TileMapCell> output;
if (!pattern->is_empty()) {
if (!p_erase && random_tile_toggle->is_pressed()) {
// Paint a random tile.
for (int x = 0; x < rect.size.x; x++) {
for (int y = 0; y < rect.size.y; y++) {
Vector2i coords = rect.position + Vector2i(x, y);
output.insert(coords, _pick_random_tile(pattern));
}
}
} else {
// Paint the pattern.
TypedArray<Vector2i> used_cells = pattern->get_used_cells();
for (int x = 0; x <= rect.size.x / pattern->get_size().x; x++) {
for (int y = 0; y <= rect.size.y / pattern->get_size().y; y++) {
Vector2i pattern_coords = rect.position + Vector2i(x, y) * pattern->get_size() + offset;
for (int j = 0; j < used_cells.size(); j++) {
Vector2i coords = pattern_coords + used_cells[j];
if (rect.has_point(coords)) {
output.insert(coords, TileMapCell(pattern->get_cell_source_id(used_cells[j]), pattern->get_cell_atlas_coords(used_cells[j]), pattern->get_cell_alternative_tile(used_cells[j])));
}
}
}
}
}
}
return output;
}
That doesn’t look too bad! But how would I draw an Ellipse? I’m neither a
computer graphics programmer by trade, nor the first person to encounter this
problem. Unfortunately, I can’t seem to locate an easy draw_ellipse
or
draw_arc
elsewhere in the Godot code-base that is easy for me to cannibalize.
A quick google search of “draw ellipse algorithm” tells me to learn more about
“Midpoint Algorithms,” specifically Bresenham’s. Time to hit the books! In
short, the algorithm seems to plot a point, see which pixel is closer, then
select that pixel. It looks like you can simplify a lot of the math as well,
particularly when working with integers. I just want to see an ellipse get
drawn, so I’m going to adapt Bresenham from zingl.
int32_t x0 = p_start_cell.x;
int32_t y0 = p_start_cell.y;
int32_t x1 = p_end_cell.x;
int32_t y1 = p_end_cell.y;
int32_t a = abs(x1-x0), b = abs(y1-y0), b1 = b&1; /* values of diameter */
int64_t dx = 4*(1-a)*b*b, dy = 4*(b1+1)*a*a; /* error increment */
int64_t err = dx+dy+b1*a*a, e2; /* error of 1.step */
if (x0 > x1) { x0 = x1; x1 += a; } /* if called with swapped points */
if (y0 > y1) y0 = y1; /* .. exchange them */
y0 += (b+1)/2; y1 = y0-b1; /* starting pixel */
a *= 8*a; b1 = 8*b*b;
do {
output.insert(Vector2i(x1, y0), TileMapCell(pattern->get_cell_source_id(used_cells[0]), pattern->get_cell_atlas_coords(used_cells[0]), pattern->get_cell_alternative_tile(used_cells[0])));
output.insert(Vector2i(x0, y0), TileMapCell(pattern->get_cell_source_id(used_cells[0]), pattern->get_cell_atlas_coords(used_cells[0]), pattern->get_cell_alternative_tile(used_cells[0])));
output.insert(Vector2i(x0, y1), TileMapCell(pattern->get_cell_source_id(used_cells[0]), pattern->get_cell_atlas_coords(used_cells[0]), pattern->get_cell_alternative_tile(used_cells[0])));
output.insert(Vector2i(x1, y1), TileMapCell(pattern->get_cell_source_id(used_cells[0]), pattern->get_cell_atlas_coords(used_cells[0]), pattern->get_cell_alternative_tile(used_cells[0])));
//setPixel(x1, y0); /* I. Quadrant */
//setPixel(x0, y0); /* II. Quadrant */
//setPixel(x0, y1); /* III. Quadrant */
//setPixel(x1, y1); /* IV. Quadrant */
e2 = 2*err;
if (e2 <= dy) { y0++; y1--; err += dy += a; } /* y step */
if (e2 >= dx || 2*err > dy) { x0++; x1--; err += dx += b1; } /* x step */
} while (x0 <= x1);
while (y0-y1 < b) { /* too early stop of flat ellipses a=1 */
output.insert(Vector2i(x0 - 1, y0), TileMapCell(pattern->get_cell_source_id(used_cells[0]), pattern->get_cell_atlas_coords(used_cells[0]), pattern->get_cell_alternative_tile(used_cells[0])));
output.insert(Vector2i(x1 + 1, y0++), TileMapCell(pattern->get_cell_source_id(used_cells[0]), pattern->get_cell_atlas_coords(used_cells[0]), pattern->get_cell_alternative_tile(used_cells[0])));
output.insert(Vector2i(x0 - 1, y1), TileMapCell(pattern->get_cell_source_id(used_cells[0]), pattern->get_cell_atlas_coords(used_cells[0]), pattern->get_cell_alternative_tile(used_cells[0])));
output.insert(Vector2i(x1 + 1, y1--), TileMapCell(pattern->get_cell_source_id(used_cells[0]), pattern->get_cell_atlas_coords(used_cells[0]), pattern->get_cell_alternative_tile(used_cells[0])));
//setPixel(x0-1, y0); /* -> finish tip of ellipse */
//setPixel(x1+1, y0++);
//setPixel(x0-1, y1);
//setPixel(x1+1, y1--);
}
It’s not production quality (it’s missing tests, doesn’t account for selecting multiple source tiles, and doesn’t implement random tiles), but it kind of works! With very limited knowledge and a couple of hours (mostly spent on trying to understand an algorithm), I was able to tweak Godot to add something I desperately wanted for years.
The Godot developers deserve some serious props for organizing their project such that a game programming ignoramus like myself could get hacking on the editor so productively in such a short time. Since I didn’t run into an organic case to cite the last two lessons, I’ll expound upon them here:
4. Take the code at its word. If a function says that it accesses data from a tile map, great! You don’t need to look at the guts of that function until you have cause to suspect it’s doing something wrong, and only do so then. Computers are largely deterministic - use this fact to your advantage. The more you can file away into your box of “knowns,” the easier it will be to navigate a code-base.
5. Code tells a story. If something looks funky in the code, assume it
exists for good reason. It’s possible the author before you was a dumb dumb no
nothing, but it’s much safer to assume they added the weird statement for a very
specific reason. git blame
can help you get to the bottom of mysteries as
well, so keep it in your tool belt!