3 min read

Lessons learned from battling Godot 3 for twelve hours

Earlier this week I spent a total of about twelve hours writing a simple Advance Wars clone in Godot. (I'm fond of Advance Wars, so I've started and abandoned a few variations of this in different engines before. This is a favorite project of mine.) It came out working pretty well, I think, by the end of those twelve hours.

I set out to write just a few basic features this week. I wanted to implement units (just one type, infantry) and map tiles (two types, grass and forest), unit movement, valid move highlighting (which mostly works), basic combat, turn-taking, terrain-based damage modifiers, a basic user interface, a title screen, and a game over screen. As a bonus, the three screens link together in a cycle: You can go from the title screen to the main game to game over and back. It's surprisingly fun for such a simple prototype.

However, the first six hours were a struggle, because I was going against the grain of how Godot wants you to do things. These are a few of the lessons I learned about how to do things "the right way" in Godot.

Lessons on Godot 3

Exported enum variables need a type hint to look nice

By default, the editor shows exported enum variables as integers. This is ugly. To fix this, you have to add a type hint to the variable, writing export (MyEnum) var value = MyEnum.FOO.

To share enums 'nicely,' you need to put them in a file with a named class

To share enums, define a class in the same script as the enum. Make sure to give the enum a name, say Bar. Then you can access the enum via the class name Foo as Foo.Bar.

This way of sharing works nicely in the editor. The same isn't true for sharing via preload, i.e. const MyEnum = preload("res://MyEnum.gd") on a file with a single anonymous enum. If you try to export a variable of type MyEnum, the script editor will complain that The export hint isn't a resource type. However, th e type hint myenum.MyEnum (if your class is named myenum) works. The script compiles and the variable shows up properly in the editor.

Thanks to this discussion post for sharing the class_name solution.

If you annotate a variable/parameter with a preloaded Enum type, you get bizarre behavior

Ordinarily, Godot won't let you annotate a variable with an enum type. That's because enums aren't "real" types. This is good.

However, if you preload() an enum, Godot will let you annotate a variable with the enum's name and this will trigger silly behavior. Suppose you write func foo(value: MyEnum). The type annotation makes foo(MyEnum.FOO) equivalent to foo(null).

I suspect this is because enums are dictionaries under the hood, not classes. Under this interpretation, Godot tries to cast the integer value of MyEnum.FOO to a dictionary type and fails, so it falls back on coercing MyEnum.FOO to null.

A script can export packed scene vars or node paths, but not node references

I wanted to export a node reference from my script, but this doesn't work in Godot. Instead, if I want a script to take an entity reference, I must provide it as a node path and a node path root.

I wanted to do a method call on a map to check if a tile is occupied by another unit already, but this turned out harder than expected. In my game, I was using a 2D grid map with units/characters moving around on that map. I wanted to check if a tile is occupied before letting a unit move there.

To do this, I needed a reference to the map script. Unfortunately, the naive way didn't work. Following advice from a search, I ended up creating a node path var and a "node path root" var and injecting them into the unit script when the unit is instanced. I needed both because you can't "resolve" a relative path to an absolute one and you also can't reorient a relative node path so that it's relative to the receiving script.

The node path + node path root approach works fine for this game, but it is pretty clunky. I imagine it may not work as well in games with more dynamic node graphs. I wonder what else would work here.

If you want to make a 2D node/area clickable, you want an Area2D/ColllisionObject2D plus CollisionShape2D

Both types provide click() signals that can link to whatever behavior you want.

If you click on an area of the screen with overlapping colliders, they all get click events

Overlapping Area2D nodes receive all click events at their position. It looks like there are a few workarounds, but I haven't tried it as it hasn't caused problems for me yet.

You can disable warnings per line with this syntax

You turn off a warning for a line using # warning-ignore:NAME. The NAME is case insensitive.