(Image credit: Godot blog)
Godot affords the possibility of unit and integration testing gdscript
code via the GUT extension. If you're getting started with unit/integrated testing (collectively "automated testing") or Godot, and interested in what automated testing can do for you, read on.
Your game contains lots of logic. When you make changes, you need to re-test that everything that worked before, still works (nothing broke). As you find and fix bugs, and get into corner-cases of your game (eg. testing the entire play-through), manually re-testing everything becomes more and more time-consuming.
Automated testing provides a solution to these problems: what if you could write code that would test your game code? What if that code could run through all the necessary scenarios of your game in a matter of minutes, or seconds? What if you could run the tests after every single change, and be sure that you never broke anything?
Automated testing provides exactly this benefit. You write test-code that tests your "production" code. Unit tests, which test very small levels of functionality, run in in a matter of milliseconds; integration tests, which typically test workflows (fight monster, get experience, level up, spend skill points) take longer, but allow you to automate workflows.
Finally, any experienced developer with automated testing will agree: the mindset change you get from automated testing is unparalleled. Not only can you test every single line of code you change, making sure it works, but you can also make bigger, riskier changes, because of your safety-net of unit tests. You can actually go faster, and make bigger changes, with your safety net of automated tests.
Like many things, automated testing is a trade-off. Unit testing is the de-facto standard now in the software industry, so we happily accept the downsides in exchange for the wealth of benefits we receive. Regardless, here they are:
Overall, the benefits heavily outweigh the costs. There's really no reason not to write automated tests!
We touched earlier on the difference between unit and integration tests. Typically, unit tests test a single method or class. The hallmark of unit tests, is that they don't depend on anything external - they don't touch the database, the file-system, they don't make web-service calls, etc. Because of this, unit tests tend to be extremely stable and robust, and run in sub-millisecond time. A large suite of thousands of unit tests may run in just a few seconds.
However, unit tests, by nature, test only a very limited set of functionality. Integration tests, in contrast, typically test more complex scenarios, and/or workflows.
Imagine we wrote an RPG-style game. The user can battle monsters; on victory, he receives experience points, and can use those to invest in learning new skills. If we structured this as typical domain-driven classes (such as Player
, Monster
, etc.) we might write unit tests such as:
Player.attack(target)
damages the targetPlayer.hurt(damage)
decreases healthPlayer.hurt(damage)
throws an error if damage is less than zero (programmer bug)These are all class/object/method-level tests. In contrast, we might write integration tests such as:
These tests, by nature, work with a larger set of code - so you can test more things. But, they can also be more fragile - if something breaks in the workflow (damage calculation broken? Level-up calculation broken? Skill-points UI broken?), the test can fail.
Godot provides two options for unit testing, depending on whether you prefer C# or GDscript.
If you prefer C#, create your project as a Godot + Mono project, and write as much logic as possible in the C# code. This way, you can use the "usual" set of C# testing tools (NUnit, Moq, etc.) and everything Just Works.
If you can't, won't, or prefer GDscript, you can use the GUT extension. (GUT stands for Godot Unit Test.)
The GUT project page on GitHub provides a detailed explanation of getting started, which I won't repeat here. Install GUT in your project, create a new scene with the Gut
object, and you're ready to go.
Instead, I'll focus on getting started testing: what do you test? Assuming you have an existing project with an extensive code-base and zero unit tests, what do you do?
Fundamentally, you need to start doing two things:
1) Refactor your code to make it more testable 2) Write tests for any new code
We didn't really touch on testable code in any depth. The short version is: write simple classes with simple methods, and avoid dependencies on any external "things." For example, if your Player.damage
class depends on a Globals.battle_difficulty
multiplier, accept it as a passed-in value instead of referencing the Globals
script. Change this:
// player.gd
class damage(target):
var total_defense = target.defense * (1 + Globals.battle_difficulty)
target.current_health -= self.strength - total_defense
// Elsewhere
player.damage(selected_monster)
To this:
// player.gd
class damage(target, battle_difficulty):
var total_defense = target.defense * (1 + battle_difficulty)
target.current_health -= self.strength - total_defense
// Elsewhere
player.damage(selected_monster, Globals.battle_difficulty)
While the code is functionally equivalent, the latter is far more testable: you can pass in whatever value of battle_difficutly
you want in your tests, even invalid values like -3! In fact, this is the kind of functionality you want to unit-test: in addition to making sure it works with a correct difficulty, you can test that it fails appropriately (eg. throws an error or returns 0) with an invalid value - something you can't easily test through the actual game.
We covered a lot of ground in this article. In summary:
I hope this post benefited you, and convinced you to at least try unit testing! Please let me know your feedback on Twitter - whether you decide to try testing, what you didn't understand, and what kind of topics you would like me to cover in future articles on Godot and testing.
I recently rebuilt a small app/game using Godot. For reference, the game contained one core loop, and took about four hours to build (from scratch). I reused all of the assets from the previous version of the game (audio and graphics), which took the bulk of development time on the original.
The game built and ran flawlessly in Godot on Windows. As soon as I ran it on Android, the game briefly showed the Godot loading logo for ~100ms, then it disappeared, then reappeared/disappeared around four times (you can see the original issue I opened about on GitHub, here).
The root cause of all this was multiple resources preloading the same script. I had a scene that preloaded HelperScript.gd
and then Second.tscn
; but Second.tscn
also preloaded HelperScript.gd
, causing the crash.
I found three different ways to address this problem:
HelperScript.gd
everywhere, I simply added it to the list of auto-loaded singletons in Godot. Problem solved. (This solution also makes sense, because I needed the same common method from HelperScript.gd
.)load
instead of preload
. This makes me think preload
is bugged; calling load
succeeds without any issues. Obviously, this has performance implications - you may want to preload your entire game at startup time.HelperScript.gd
into two different scripts, and just load what I need instead.If you find your Godot game crashes on startup without explanation, maybe this is something you can investigate. (Failing that, go for a git bisect
and try to isolate the commit that broke everything.)
Recently, I was tasked with learning ReactJS (or just "React"). So, I made the most obvious choice possible: I decided to write an ASCII roguelike, which uses React as the front-end.
Why? For several, obvious reasons:
I set out to create, in my mind, the first step of any text roguelike: draw text on the screen really fast and see how it performs. HTML should render at lightning-speed in my modern browser, right?
Wrong.
React is just a front-end technology; it doesn't dictate how I should structure my HTML. With that in mind, I set out to answer two questions:
div
tags vs. span
tags, etc. make any difference?In brief, the answer to question #2 ended up being "no." Performance bottlenecks on something else entirely; none of those changes make any significant difference.
I wrote a little Javascript that tests exactly what I want:
div
tag for each tile. Style it appropriately (eg. width/height big enough to fit any character)You can see the code, more or less (with FPS counting), here. It's simple, and to the point.
In a word: abysmal! On my fairly beefy dev machine (lots of RAM, good CPU, mediocre GPU), it renders at a measly 6-7FPS. You can see the results in this tweet.
As I mentioned earlier, I tried several variations; none of them improve performance, at all. The core of it comes down to a call to set the character itself. Pseudocode:
var character = ...
var colour = ...
var tile = window.tiles[i];
tile.innerText = character
It turns out that browsers, even modern ones, even on beefy hardware, are really, really slow when you set innerText
. The only alternative - using innerHTML
-- is slower, and probably broken on Internet Explorer.
For an ASCII roguelike without too much going on, 6FPS is probably enough. If I really cared about performance, I could switch to canvas-rendering and a bitmap font (lots of work and not sure how it works with React), or using images - either images of text, or real images.
For me, the goal is to learn ReactJS, so I plan to continue forward with this as-is, without major surgery.
Godot's tile editor provides you with the ability to quickly make 2D tile-maps, similar to RPG Maker. This makes it easy to design and tweak maps. But can we take it a step further? Can we, in fact, use the tilemap editor to create functioning entities -- making it a domain-specific editor for our specific game?
We can. This post walks through a lightning-quick method you can use.
Godot's Tilemap
entities are simple. They're essentially a reference to a tileset (graphic with all your tiles on it), and one or more layers of those tiles drawn on a grid. In Godot, tilemaps are all about presentation.
What if you want to add functionality though? Imagine you're creating a 2D adventure or RPG (like, hmm, Eid Island maybe?), and you want to be able to draw enemies on the map. Or trees you can chop down, coins you can pick up, or anything else.
By itself, Godot doesn't provide a way to do this. However, I asked around on Discord, and checked some issues on GitHub about this; a definitive, simple approach appeared, something like a best-practice. How? Easy.
Scene
instances)Godot makes this process quite easy. As a pre-requisite, I would recommend:
Tree
or Plain Water
)Tree
scene (subclasses Node2D
, has a Sprite
, script, etc.)var entity_tiles = { "Tree": preload("res://Scenes/Entities/Tree.tscn") }
With this in place, we simply iterate over the tiles and -- if the name appears in our dictionary -- replace the tile with its corresponding entity. Here's an early reference implementation in Eid Island.
# Find entities on the map (eg. trees). Remove them and replace them with
# real entities (scenes) so that we can have logic (attach scripts) to them.
func _populate_entities(map):
var possible_tilemaps = map.get_children()
for tile_map in possible_tilemaps:
if tile_map is TileMap:
var tile_set = tile_map.tile_set
for cell in tile_map.get_used_cells():
var tile_id = tile_map.get_cellv(cell)
var tile_name = tile_set.tile_get_name(tile_id)
if entity_tiles.has(tile_name):
# Spawn + replace with entity of the same name
var scene = entity_tiles[tile_name]
var instance = scene.instance()
map.add_child(instance)
instance.position.x = cell.x * Globals.TILE_WIDTH
instance.position.y = cell.y * Globals.TILE_HEIGHT
# Remove tile
tile_map.set_cellv(cell, -1)
Globals.TILE_WIDTH
and TILE_HEIGHT
refer to the (fixed) tile size; you can, alternatively, use the tileset/tilemap to get the cell size, or use the entity size as a reference (although the entity should be exactly one tile size for that to work).
With this in place, you can quickly and easily create levels with real functionality from simple tilesets.
One obvious missing piece of functionality is customization. What if my tile is, say, a door that warps somewhere, and I want to specify the destination properties when I draw the tile? How can I do that?
Unfortunately, I don't know the answer yet. As far as I know, Godot doesn't allow you to add additional variables/properties to the tile itself. Perhaps you could store the data elsewhere, such as a separate dictionary mapping entity/coordinates to custom data.
If you know how to solve this latter problem, drop us a note on Twitter or in the Godot Discord chat so we can use it too!
This month, I set out with a very specific goal: create a 2D procedurally-generated RPG; something I've never heard of (or done before). Although I ran out of time, I plan to devote another month to the game. This retrospective discusses the good, the bad, and the work ahead of us.
I grew up playing Secret of Mana, Final Fantasy, Chrono Trigger, and other great titles. One day, I thought to myself, what if I can only create one more game before I die? What would I create?
The answer? Something akin to those favourites; a game that generates a brand new, quality RPG, every time you plan. I spent some weeks brainstorming what I needed and didn't need, plans to create content such as monsters, weapons, etc. Some things clearly need to be procedurally-generated (such as dungeons and maps), while others don't (such as enemy sprites and player skills).
A couple of false-starts later, I finally opened up the Godot editor and started the third (or fourth) attempt.
Initially, I focued on the technical side of procedural generation: creating stuff! As of writing, the game:
The forest dungeon took longer than expected. On the technical side, I figured out how to make Godot generate and persist my maps/game; initially with packed scenes (which got garbage-collected when I changed scenes, causing crashes), and later by separating the generation of game data from the presentation (populating scenes/tilemaps/etc. from content).
Solving technical issues aside, I also spent too much time on what didn't go so well: the battle engine.
I find the concept of memory games fascinating (as an ex-Lumosity player). An Extra Credits video on puzzle vs. reflex mechanics challenged me to introduce something new into my game.
At the heart of every RPG, in terms of fun gameplay, lies a fun and challenging battle system. I decided to design a memory-based battle system:
This probably proved to be my undoing. I spent several days designing, balancing, and tweaking the battle engine; adding new features, action types, and skills. Five beta testers provided feedback after trying it out:
In the end, after several days, I forced myself to stop tweaking/changing it and decided to focus on fulfilling the rest of the game.
Last, and perhaps worst, the game feels incomplete. Sure, you get a randomly-generated forest; but even with the intended scope for the end of the month (two dungeons with two different variations, including unique monsters), it might not be enough to give players that experience of a procedurally-generated RPG.
I don't intend to give up; I plan to fill out and ship a version of this game (although with what functionality/content remains to be seen). My current plan of action looks like:
Finally, I plan to create a proper RPG world. A good RPG, like a good novel, contains elements that all harmonize and work together to communicate a singular world/message. Right now, we have a patch of hastily-named enemies, skills, etc. and these need to be brought together.
More specifically, I didn't yet start the other hard/fun part of a procedural RPG: creating a procedural world (and story) that feels unique. This includes a unique/different protagonist for each story.
Finally, another round of beta testing should reveal whether the game is interesting/fun or not, and I should either finish it as-is or if I need to rewrite/rethink battles entirely. Time will tell. September may be almost gone, but October brings another month brimming with potential for me to see this game through to its potential.
If you're interested in following my development on this project, feel free to follow me on Twitter.