Godot is very intuitive once you know that it's 'Scenes' all the way down. Its engine-specific scripting language, GDScript, can feel a bit wonky at times but it's also very fit for purpose β as long as you don't fall into the trap of thinking that it's Python despite its syntactic similarity. I can see it being useful to drop down into a different language at times (C#, C++ are supported but others are available, including Rust), but GDScript was perfectly adequate for me and I'd probably advise against getting too complicated in a short event like this.
Basically every tool has quirks and a good craftsperson would do well to become aware of them. Here are a few traps I fell into and a few tricks I learnt about.
yield
and SceneTree timers combined seem dangerousπAt some point if you dig deeply enough, you'll run into GDScript's yield
keyword which lets you defer execution until another event triggers.
You might be tempted, as I was, to use this sort of snippet for inserting a sleep into your code to make something happen later:
yield(get_tree().create_timer(5.0), "timeout") # wait 5 seconds before next event
This works, but it turns out that if the current object/Scene gets removed from the world during the sleep, the resumption of the script will lead to an error: Resumed function after yield, but class instance is gone.
. I didn't actually notice any ill effects from this β just an error in the console and everything else continued as usual, but it's poor to have errors showing up when things are working correctly and I can't guarantee that this wouldn't lead to some footguns in other situations either.
The slightly unfortunate workaround for this is to use an explicit Timer
node as a child of your Scene
. If necessary, this means creating a Timer and remembering to remove it afterwards.
var timer = Timer.new()
timer.one_shot = true
$Timer.add_child(timer)
timer.start(time); yield(timer, "timeout")
$Timer.remove_child(timer)
It's safe to use yield
on this timer node because once the current Scene
is destroyed, the Timer
will also be destroyed as it is a child and the script will never be resumed as a result. Note: a Timer
can't have multiple timeouts, so you need one per task... I'd be tempted to wrap this up in a neater package if I was writing a proper game and needed to make use of this, but for the purposes of the game jam I took the lazy way out.
Source for this trick on the Godot Engine Q&A site β thanks k2kra!
My game is 2D but set with a perspective that I don't know the term for, but the best I've found is 'top-down oblique'. It's as though you have an orthographic camera (so no vanishing point and no concept of things getting smaller as they get further away) angled so you can see the top and front of things. It's convenient for drawing because you can draw terrain as thought it was top-down, characters as though you are looking at them from the front and structures as a bit of both β and everything will look fairly reasonable. Structures can be built out of reusable tiles (in Godot: use a TileMap
!) which greatly cuts down on time needed to produce levels.
A slight problem emerges because characters can walk in front of and behind other characters and objects in the world. Despite thinking you had a 2D game, you now need to worry about ensuring that everything renders in the right order so things that are logically in front appear in front.
I never got this exactly right during the event itself, but I at least know some tricks.
The biggest trick: use the YSort
node. YSort
sits in your scene tree and re-orders its children depending on their Y-position. For example, when the player walks in front (below) of a tree, they show up in front, but when they walk above, they show up behind the tree.
For this to work properly, you absolutely must make sure the origin point of your child scenes/entities matches where you want the ordering to be decided! Examples that would seem typically correct would be to put the origin at a character's feet, or at the base of a tree, or the base of a wall. Essentially: wherever the entity touches the ground.
In my case, I started off my game with little regard for the origin positions of objects, using explicit z-indices to solve all my ordering problems and designing levels so that you never noticed any discrepancies. Eventually, I ran into a case I couldn't design away and learnt about YSort
, but then it was difficult to rework all my objects and I released the game with some broken Z behaviour (switching to YSort
fixed some of my issues but broke at least one case I worked around manually beforehand).
An especially tricky case, present in my game, is that of objects which can have other objects walk or sit inside: for example, the player can walk in the back of the postal van/truck and the olives can sit inside the wheelbarrow. It's easy enough to split the front and back of these container objects into different sprites (if you drew them with layers in Krita, you can just export different layers/layergroups), but z-ordering them is tricky.
I still need to research the proper way to support these 'container' objects, particularly the ones in the world: maybe there's a trick hidden by clever z-indexing or the 'Show Behind Parent' option?
I'd also note that I don't know how to make TileMaps interact with the YSort
node's sorting such that you could have walls that occlude the character behind but do not cover the character in front: I just made it so that you couldn't walk into any section of the walls.
To make my game come alive a bit, I wanted at least one kind of 'enemy' character: in the time available, this turned out to be slugs. They wouldn't add much to the game if they couldn't walk around though, so I needed pathfinding. Godot provides the 'navigation' system for this purpose.
With that said, I found documentation a bit hard to come by for this.
Most online sources point you to the Navigation2D
node (you'd use it as a parent of your world). At least in Godot 3.5.1, this node is deprecated with a (somewhat misleading/unhelpful) message pointing you to the Navigation2DServer
. But the online sources I came across, even for Godot 3.5.1, still use that node claiming it is more configurable. I know it's a game jam, but I didn't particularly want to drop a deprecated component in my project.
I had a few cases of the navigation system randomly not working, but here are some rough notes:
Put a NavigationAgent2D
node in your enemy (or whatever it is that needs to walk around) scene
In your levels (or potentially in the tiles, but I didn't figure this out in time), add some NavigationPolygon
s that cover the area in which the enemies are allowed to walk. Keep the gap between them quite tight and also don't overlap them too far β it seems like the lines need to be close together for them to join up.
Because you're (likely) a human β who didn't make an absolutely perfect and gapless navigation mesh β increase the edge connection margin of the navigation system:
# random place to put it, but needed to get our navigation working:
var w2d: World2D = get_world_2d()
Navigation2DServer.map_set_edge_connection_margin(w2d.navigation_map, 10.0)
(In this case, this means 10 pixels is the allowed margin between two polygon's edges for an enemy to be allowed to walk across.)
If you don't do this, your navigation polygons need to be extremely-well (perfectly?) lined up. This is the reason that some sources are still using Navigation2D
nodes, because you can easily configure this in the editor there. I couldn't find anywhere to configure this on the Navigation2DServer
, so I put the above in the _ready
function for my UI code (which is present on every single level). Messy, but works...
Occasionally (on a timer) poke the NavigationAgent2D
to navigate to your target (in case the target moves, like a player) and have your physics processing code steer the enemy towards the next point β crucially, you call NavigationAgent2D.get_next_location()
to get the global position of where you need to go to follow the path.
I'd recommend that anyone interested in this feature experiment with it before a game jam if they're interested in not wasting an hour or two learning it on the job...
I don't know enough about the right way to structure a Godot project and especially not about how to effectively reuse code/component Scenes.
For example, if I wanted to add an upgraded wheelbarrow that can hold more olives, I'm not sure about the cleanest way of doing that.
For my game, I used a lot of collision layers despite it being a short project. Although you can name them (which is a nice touch!), there are a finite number so I'd probably want to think more carefully about how to share layers for multiple uses in a proper game.
Here are the other tools I used:
There's an extra hour for submission. I used it for packaging up my game and writing up a description. I did not know that I would need a cover photo, so I just took a screenshot of the game, but in future I would try to make a bit more of a polished cover photo (observing the size requirements for submission).
I used Godot's 'Export' wizard to create a HTML5 export with the main name of index.html
(as needed for Ludum Dare's embed). I also placed a copy on my personal website so people could play the game fullscreen. Note that there's a fairly chunky download to complete before you can export a HTML5 game for the first time; if on a slow network it'd be a good idea to have that already done in advance.
Don't make the mistake of thinking that the event only lasts 73 hours... because after the deadline, it's time to play other people's games and rate them! By doing so, you also encourage your game to show up at the top of the list for other players to find (which then leads to getting more ratings); note that you need 20 ratings to get a rank at the end of Ludum Dare!
I'd say, based on just my experience, you should expect to play and rate about fifty games in order to attract enough attention to obtain your basic 20 ratings β but I noticed other games had a much better ratio, so perhaps my game was just not very enticing (hint: maybe a better cover photo would have helped!). There were many interesting and well-polished games with interesting takes on the theme, but of course there were also some games you might consider underdeveloped, unoriginal or buggy.
Playing the other games was also a good chance to reflect on my own game. Some rough points:
It seems that some people will continue working on their games after the jam. I know I was intending to, but reality gets in the way. Ludum Dare is a special time. My recommendation (mostly to myself) is to make time for it, then make the most of it β because that special time is not forever. I'd suggest trying to get your game to a state you are content with, before the jam ends and real life resumes.