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.
And we get the following, just as weâd expect:
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:
In your âmainâ folder, create a file named âmain.cppâ with the following contents:
Open Visual Studio, and select âFileâ -> âOpen CMakeâŚâ -> <select your main folder>
In Visual Studio, click on the Configuration drop-down in the main toolbar and choose âManage ConfigurationsâŚâ
In âCMake toolchain file:â, paste <vcpkg-clone-dir>/scripts/buildsystems/vcpkg.cmake
Click on the Play button at the top of the page. You should see a console window containing the text âHello, world!â);