I’ve been thinking a lot about software developer archetypes recently. The way I see it, there are 3. I call them the
trailblazer, the artist, and the engineer.
The trailblazer just wants to see something work. They don’t sweat the details. Getting something on the screen makes
them happy, and visible progress in turn makes marketing happy. Trailblazers are happy to glue together libraries. They
are the most likely to enjoy agentic workflows. If AI generates what appears to be a working system, that’s perfect -
the customer asked for X, and this fits the shape of X.
The artist is the exact opposite. They sweat every detail. These tend to be backend folks, as frontends aren’t what
appeals to them - an elegant system is. They love the craft of coding for its own sake. Artists can burn a full day on
API design. They probably care less about the schedule then they should. However, when they build systems you can count
on them to be rock solid.
The engineer cares about the process. They will make sure you are following the necessary standards and regulations.
Engineers sweat the requirements and the schedule. They are rule followers, so they will attempt to design up front and
build a well tested system. However, they are also pragmatic. They will time-box initiatives, and are willing to accept
some technical debt to cross the finish line. This happy medium makes them the most likely of the 3 to be a productive
manager.
None of these archetypes are inherently better than the other. A large organization will need all 3 to succeed. The
goal of people management is to allocate them where they belong.
You want to allocate your engineers to positions where they can manage stakeholders and build the core of individual
products.
You want to allocate your artists to organization-wide middleware, libraries, and services.
You want to allocate your trailblazers to feature development - the leaf code that customers want, but other developers
don’t need to build on.
People managers fail when they mis-allocate. When they see a trailblazer create many lines of code and put them on
libraries. When they see how meticulous an artist is and assumes that passion translates to people management. When
they see an engineers flexibility but constrain them to a box of just writing tests.
Here is a list of all of the game development tools that I find interesting as an amateur.
Tools are organized into sections with brief explanations. Items are roughly sorted in
order of my personal preference. Additionally, some entries will have icons
next to them. The key is as follows:
💸 The tool in question has a monetary cost. They still may include free options.
🐣 I believe the tool in question is very good for beginners.
❗❗ The tool in question is more geared towards experienced users.
⏲ The tool is in beta - not ready for full use, but worth keeping tabs on.
Development
Game Development can generally happen at 3 levels of abstraction:
Game Engines, which contain integrated visual editors and are jam packed with
tools out of the box.
Frameworks, which are large, opinionated code libraries that help you rapidly
code a game from scratch with basic functionality like graphics, audio, and basic
networking
Libraries, which are generally single purpose for a particular programming
language. For example a game may opt to use a particular library specifically for
loading png images from disk into a format usable by their game.
Fair warning that the lines between these can get blurry.
This 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:
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:
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.
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:
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”:
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.
boolTileMapEditorTilesPlugin::forward_canvas_gui_input(constRef<InputEvent>&p_event){// ...Omitted for brevity...}elseif(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();}elseif(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.
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(Vector2ip_start_cell,Vector2ip_end_cell,boolp_erase){TileMap*tile_map=Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id));if(!tile_map){returnHashMap<Vector2i,TileMapCell>();}Ref<TileSet>tile_set=tile_map->get_tileset();if(!tile_set.is_valid()){returnHashMap<Vector2i,TileMapCell>();}// Create the rect to draw.Rect2irect=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.boolaligned_right=p_end_cell.x<p_start_cell.x;boolvaligned_bottom=p_end_cell.y<p_start_cell.y;Vector2ioffset=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(intx=0;x<rect.size.x;x++){for(inty=0;y<rect.size.y;y++){Vector2icoords=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(intx=0;x<=rect.size.x/pattern->get_size().x;x++){for(inty=0;y<=rect.size.y/pattern->get_size().y;y++){Vector2ipattern_coords=rect.position+Vector2i(x,y)*pattern->get_size()+offset;for(intj=0;j<used_cells.size();j++){Vector2icoords=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])));}}}}}}returnoutput;}
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_tx0=p_start_cell.x;int32_ty0=p_start_cell.y;int32_tx1=p_end_cell.x;int32_ty1=p_end_cell.y;int32_ta=abs(x1-x0),b=abs(y1-y0),b1=b&1;/* values of diameter */int64_tdx=4*(1-a)*b*b,dy=4*(b1+1)*a*a;/* error increment */int64_terr=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!
First off, I’d like to preface this discussion by saying DO NOT DO THIS. There is currently a proposal to deprecate the use of the comma operator in subscripting expressions in order to allow for proper multidimensional overloads. This would allow us to write array2d[0, 0] rather than needing to rely on undesirable type nesting (for array[0][0]) or “abusing” the function call operator the way Eigen does array2d(0, 0). In section 4, the proposal notably mentions the parsing library Boost Spirit Classic (deprecated since 2009) as one of the few real-world users of this comma abuse technique. Now, with that out of the way:
How many times are you working with your home-grown 2D matrix class and you accidentally mixed up indexing by X/Y vs indexing by Row/Col? Just a few short weeks ago I stumbled on this when rigging up a quick Tetris game. Indexing std::array<std::array<T, COLS>,ROWS> arr with arr[x][y] isn’t just incorrect, it’s embarrassing. Now, I of course read paragraph 1 of this article, so I didn’t actually do this, but I did wonder if there was a way to leverage C++’s type-safe mechanisms to prevent me from making this mistake at compile time… (it’s C++, so the answer is obviously “yes, with some effort.”)
Using the power of user defined literals and overloading the comma operator, we can simulate multi-dimensional subscript operators, and overload on types! We simply make types for Coordinates in forms of X, Y, Row, and Column. Next, create our user defined literals for convenience. Finally, create our comma operators and leverage a common MatrixIndex type, which interprets the coordinates in a common form.
For bonus points, compile with C++17 to get constexpr subscript operators on std::array. And vuala, you now have the power to perturb friends and coworkers alike.
So you want to start programming with C++ and you’re using Windows. Great! Microsoft’s reputation for incredible developer tools is well earned. We’re going to get set up with Microsoft Visual Studio (note that this is different than vscode), the Microsoft Visual C++ Compiler (MSVC), the CMake meta build system, vcpkg package manager, and Git version control. If you don’t know what some of that means, worry not! We’ll go step by step!
Go to https://visualstudio.microsoft.com/ and download “Visual Studio 2019 - Community”. This will be our integrated development environment (IDE) for C++. Like Microsoft Word, but for code. The community edition is free for hobbyists.
You should see a screen that resembles the following:
If you’re crunched for disk space, you can just select the following:
* "MSVC v142 - VS 2019 C++ x64/x86 build tools"
* "Windows 10 SDK"
* "C++ CMake tools for Windows"
Don’t worry if the versions aren’t exactly the same.
Optionally, if you’re interested in programming for Linux or IoT, select the following:
* "C++ CMake tools for Linux"
* "Embedded and IoT development tools"
Click “Install” and buckle up–the download will take some time.
Install a git client (if you’re already familiar with git, a client is not necessary). Git is a popular “source control” program that helps developers track revisions of the code they make, collaborate, and keep backups. Here are some popular options that are free for hobbyists:
Standard installation options for any of these should be fine.
Clone the vcpkg repository using your git client. This is a “package manager”–it will install useful libraries that will let us do more with less code. Open the clone menu for your git client, and provide the following link: https://github.com/microsoft/vcpkg.git.
GitKraken:
Click “File”
Click “Clone”
Click “Clone with URL”
Paste the vcpkg link in “URL”
Click “Clone the repo!”
Source Tree:
Click “Clone/New”
Click “Clone Repository”
Paste the vcpkg link in “Source Path / URL”
Click “Clone”
SmartGit:
Click “Project”
Click “Clone…”
Paste the vcpkg link in “Repository URL:”
Click “Select”
Click “Next”
Click “Finish”
Note the path of the directory vcpkg was cloned to.
Launch the command prompt. You can do this by right clicking the start menu, selecting “Run”, typing “cmd”, and clicking “OK”.
In your cmd, type cd "<path-to-vcpkg>", where <path-to-vcpkg> is the directory you cloned vcpkg to.
In your cmd, run .\bootstrap-vcpkg.bat
In your cmd, run .\vcpkg integrate install
In your cmd, run .\vcpkg install gtest:x64-windows
We have just installed our first package–Google’s very own unit testing framework!
Create a folder to put your C++ project in. I’ll name mine “main”.
In your “main” folder, create a file named “CMakeLists.txt” with the following contents: