<![CDATA[Five Nine Games]]>https://blog.fiveninegames.io/https://blog.fiveninegames.io/favicon.pngFive Nine Gameshttps://blog.fiveninegames.io/Ghost 6.0Mon, 08 Sep 2025 13:54:06 GMT60<![CDATA[Adding Dynamic Resolution in Godot]]>https://blog.fiveninegames.io/adding-dynamic-resolution-in-godot/68bdc7c86e4f300001dd2eedWed, 03 Sep 2025 11:21:34 GMT

As mentioned in the Previous Post, an option for dynamic resolution has been added to our upcoming game. This allows the optimal resolution scale to be identified at runtime, helping it run smoothly on a wider range of hardware.

The primary cause of the performance issues on macOS was the notably high resolution of the HiDPI screen. Since the MacBook used for testing had fixed specifications, we could find an optimal resolution scale for that device. However, using a single scale across all Apple devices would be less than ideal for players with higher-end machines - enter dynamic resolution scaling.

Resolution scaling is quite common in most games nowadays. It offers the ability for users to reduce the rendering resolution of the game without having to endure annoying resolution changes when alt-tabbing out of a game. The naming convention is not always consistent, for example, in Far Cry 6 it is known as "adaptive resolution", whereas in Final Fantasy XIV it is known as "dynamic resolution", but they all do the same thing.

Adding Dynamic Resolution in Godot
The settings screen of Far Cry 6 containing the settings for resolution scaling

By creating a node that is capable of monitoring the frame rate of the game against a target frame rate, it's possible to have the game itself find the ideal resolution scale, without having to expose a resolution scale setting that may not be ideal for users who:

  1. Don't fully understand what the graphics settings are outside of using presets like "Low", "Normal", "High", "Ultra High", "Super Ultra High Turbo Alpha 2"
  2. Don't want to have to fine-tune a slider that exposes the full range of values from 0.0 to 1.0 (or higher if you want to enable super sampling).

The current implementation of the resolution scaler can be found below. It includes 3 properties that need to be set in the editor:

  • Frames to Ignore: when your game is initialising, there's going to be some low reported frame rates. If you don't ignore an arbitrary number of frames, the resolution scaler will start to reduce the resolution scale far below what the user needs. The optimal amount found during testing for this is 120 and is the default value.
  • Maximum Scale: if the frame rate is above the target frame rate, the scaler will begin to raise the resolution scale again. If you want to avoid super sampling, then leave this set to 1.0.
  • Target FPS: the FPS that the scaler should aim to bring the user toward.
## A utility node that will poll the frame rate of the game every second
## and change the resolution scale accordingly to attempt to keep the game
## running at the desired frame rate.
class_name ResolutionScaler
extends Node


@export var enabled: bool = false

## The number of frames to ignore on startup before scaling the resolution.
## [br][br]
## [b]Note[/b]: This will reduce the likelihood of unnecessarily reducing the
## scale to very low values during initialisation.
@export var frames_to_ignore: int = 120

## The maximum scale value to be applied once the target frame rate is reached.
@export var maximum_scale: float = 1.0

## The FPS that the scaling aims to achieve and maintain.
@export_range(20, 200, 1.0) var target_fps: int = 60

var _ignored_frames: int = 0
var _timer: Timer


func _ready() -> void:
	_timer = Timer.new()
	_timer.wait_time = 1.0
	_timer.timeout.connect(_on_timer_timeout)
	add_child(_timer)


func _process(delta: float) -> void:
	if _ignored_frames < frames_to_ignore:
		_ignored_frames += 1
		return

	if enabled and _timer.is_stopped():
		_timer.start()
	elif not enabled and not _timer.is_stopped():
		_timer.stop()


func _on_timer_timeout() -> void:
	var fps := Engine.get_frames_per_second()
	var viewport := get_viewport()
	var adjustment_multiplier: float = fps / target_fps

	viewport.scaling_3d_scale = minf(
		viewport.scaling_3d_scale * adjustment_multiplier,
		maximum_scale
	)

Any updates to this script will be published on GitHub in the following repository: https://github.com/FiveNineGames/godot-resolution-scaler

To use it, simply drop the script [resolution_scaler.gd] in your preferred location and add a ResolutionScaler to your scene. Set up the properties as explained above, and the scaler will dynamically handle everything for you as long as the enabled property is set to true.

Alternatively, you can install directly from the AssetLib tab by searching for "Resolution Scaler"

]]>
<![CDATA[Adding Support for macOS]]>https://blog.fiveninegames.io/adding-support-for-macos/68bdc7c86e4f300001dd2eecMon, 01 Sep 2025 14:33:00 GMT

It's been a while since the last blog post, but work hasn't slowed down! A lot of progress has been made on one of the boss fights, several music tracks are now complete, and a number of performance improvements have been implemented to help reach a wider audience at release. Which brings us to the topic of this post: macOS is now on the roadmap!

A build was tested on a MacBook Air some time ago, but the performance was terrible. While working on optimisations for the Steam Deck, macOS was revisited and the source of the issue became clear.

As you can see in the video below, before optimisations were applied the frame rate hovered around 30 FPS, sometimes dropping as low as 28 😬. In contrast, after the adjustments, it now holds a consistent 60 FPS:

0:00
/0:24

The primary cause of this (which a regular macOS user might have spotted more quickly) was the HiDPI screen. Although it's only a 15" screen, the resolution sits at 2880x1864 - that's a lot of pixels to push!

The solution was to find a sweet spot with in-game resolution scaling; lowering the internal render resolution just enough to maintain visual quality while hitting a steady 60 FPS ☺️.

Rather than limiting this optimisation to MacBook Airs, however, a new dynamic resolution setting has been added for all platforms. This will help lower-end devices maintain smoother frame rates without sacrificing too much on the visuals.

For fellow developers - check back soon. A follow-up post will cover how to implement this yourself (assuming you’re using Godot).

]]>
<![CDATA[Evading GridMap Crashes]]>It's been another busy streak since the last update including a rework of all the existing levels. In the process of doing this, a not-so-obvious bug revealed itself in Godot.

As mentioned in The Previous Post, several levels were reworked to use new wall meshes. While doing this,

]]>
https://blog.fiveninegames.io/evading-gridmap-crashes/68bdc7c86e4f300001dd2eebFri, 18 Apr 2025 22:23:05 GMTIt's been another busy streak since the last update including a rework of all the existing levels. In the process of doing this, a not-so-obvious bug revealed itself in Godot.

As mentioned in The Previous Post, several levels were reworked to use new wall meshes. While doing this, some were rearranged slightly and needed some areas of the map to be moved. While doing this, the editor consistently crashed when bulk moving tiles with the following stack trace:

ERROR: Requested for nonexistent MeshLibrary item '1'.                                                           
   at: get_item_mesh (scene/resources/3d/mesh_library.cpp:221)

================================================================
handle_crash: Program crashed with signal 11
Engine version: Godot Engine v4.4.1.stable.official (49a5bc7b616bd04689a2c89e89bda41f50241464)
Dumping the backtrace. Please include this when reporting the bug to the project developer.
[1] /lib/x86_64-linux-gnu/libc.so.6(+0x45810) [0x75d825645810] (??:0)
[2] ~/.local/bin/godot() [0xf9a45b] (??:0)
[3] ~/.local/bin/godot() [0x4513ff4] (??:0)
[4] ~/.local/bin/godot() [0x47d555a] (??:0)
[5] ~/.local/bin/godot() [0x80ba73] (??:0)
[6] ~/.local/bin/godot() [0xff6b76] (??:0)
[7] ~/.local/bin/godot() [0x18d15b6] (??:0)
[8] ~/.local/bin/godot() [0x1fcbfc8] (??:0)
[9] ~/.local/bin/godot() [0x1fa8d7c] (??:0)
[10] ~/.local/bin/godot() [0x47d555a] (??:0)
[11] ~/.local/bin/godot() [0x2a3ac76] (??:0)
[12] ~/.local/bin/godot() [0x2985808] (??:0)
[13] ~/.local/bin/godot() [0x29865b6] (??:0)
[14] ~/.local/bin/godot() [0x299d7bc] (??:0)
[15] ~/.local/bin/godot() [0x4f18ecb] (??:0)
[16] ~/.local/bin/godot() [0x4ced85] (??:0)
[17] ~/.local/bin/godot() [0x442a293] (??:0)
[18] ~/.local/bin/godot() [0x442c14f] (??:0)
[19] ~/.local/bin/godot() [0x4dc1e3] (??:0)
[20] ~/.local/bin/godot() [0x41e7a0] (??:0)
[21] /lib/x86_64-linux-gnu/libc.so.6(+0x2a338) [0x75d82562a338] (??:0)
[22] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x8b) [0x75d82562a3fb] (??:0)
[23] ~/.local/bin/godot() [0x44297a] (??:0)
-- END OF BACKTRACE --
================================================================

As it turns out, when some old items were removed from the mesh library, they hadn't also been removed from the GridMap that was in use first; this leaves a lingering reference in the GridMap to items that no longer exist in the mesh library.

In the below example, you can see we have three distinct meshes; a blue (item 1), a red (item 2) and a green (item 3) cube:

A GridMap before deleting any items from its assigned mesh library

If we leave the GridMap as is, and delete item 2 from the assigned mesh library, the GridMap will visually update to reflect this, as can be seen below, but the cells between the blue and green cells will still reference item 2 of the mesh library.

The GridMap after deleting the red mesh from the mesh library but not from the GridMap itself

Even though these items no longer exist in the mesh library and show no visible mesh on the GridMap, the cells containing the invalid references will still attempt to access the item when moving them after selecting an area that contains them, which then causes the crash.

An issue for this has been raised for this on GitHub (which you can track Here), but in the meantime, you can cleanup GridMaps containing invalid items using the script we've created below:

@tool
class_name GridMapCleaner
extends Node


@export_tool_button("Remove Invalid Items", "Callable") var remove_invalid_items_action: Callable = remove_invalid_items

@export var grid_map: GridMap


func remove_invalid_items() -> void:
	if not grid_map:
		print("GridMapCleaner: GridMap has not been set, aborting.")
		return

	var mesh_library: MeshLibrary = grid_map.mesh_library
	var used_cells: Array[Vector3i] = grid_map.get_used_cells()

	for i in range(used_cells.size()):
		var cell: Vector3i = used_cells[i]
		var item_index: int = grid_map.get_cell_item(cell)

		var mesh: Mesh = mesh_library.get_item_mesh(item_index)

		if not mesh:
			print("GridMapCleaner: Removing invalid mesh ID (", item_index, ") at ", cell)
			grid_map.set_cell_item(cell, GridMap.INVALID_CELL_ITEM)

	print("GridMapCleaner: Finished removing invalid items.")

To use this tool, create a new GDScript file wherever you prefer to store your tools and paste the above code into it.

Next, go to the scene containing a GridMap that you want to clean of invalid items and add a new GridMapCleaner node. If you open the GridMapCleaner node in the inspector, you'll see a "Grid Map" property [1] that you can assign your problematic GridMap to and a button [2] that can be used to remove all the invalid items:

The GridMapCleaner inspector pane

After hitting the Remove Invalid Items button, you should see a message in the output pane indicating it is complete, and the items should no longer cause the aforementioned crash or any other unexpected issues.

]]>
<![CDATA[Knocking Down The Walls & Retexturing]]>We're getting closer and closer to finally getting the store page up [for wishlists, release date is still TBC] and getting the trailer ready; but unfortunately, things have taken a few steps backwards in the last couple of weeks.

One of the primary issues that needed to be

]]>
https://blog.fiveninegames.io/knocking-down-the-walls-retexturing/68bdc7c86e4f300001dd2eeaSat, 05 Apr 2025 13:33:31 GMT

We're getting closer and closer to finally getting the store page up [for wishlists, release date is still TBC] and getting the trailer ready; but unfortunately, things have taken a few steps backwards in the last couple of weeks.

One of the primary issues that needed to be addressed is the walls of the arenas. The levels are built using a library of modular models, and ideally, we wanted to be able to reduce the number of models and just be able to swap out textures to switch between the different themes. Unfortunately, some early decisions caused issues in doing this in terms of how they join up. There were a few little visual tricks/hacks that had mitigated this, but they'll unfortunately not work for all the level themes, which has meant we've had to scrap the original walls and start again.

In our case, this has thankfully not been a huge undertaking, as the rest of the world has kept a consistent scale and we only have 10-15 pieces for the walls that need reworking; but, for anyone embarking on a bigger project that has a more dynamic range of objects (in terms of their scale), making this same mistake could be very costly.

Something that has been a huge help during this process, is yet another free asset pack from Kenney - the Prototypes Textures pack, which you can download From Here.

Knocking Down The Walls & Retexturing

Using these prototype textures, it was possible to perfectly unwrap our models and keep a consistent scale between every modular wall, allowing for them to seamlessly join together.

Knocking Down The Walls & Retexturing
A small test scene using the prototype textures

A huge thank you to Kenney for these textures, the assets they create and make freely available to the game dev community are amazing and I would encourage anyone who can to head over to their Itch page and consider spending a bit of money to support them.

]]>
<![CDATA[Baking Mesh Skeleton Poses in Godot 4.4]]>We're almost done with developing the environmental hazards and this last one proved a bit more difficult than anticipated. Thankfully, Godot 4.4 released a few days prior to starting the work on this [great timing], which had some additions that helped overcome the main issue.

The hazard

]]>
https://blog.fiveninegames.io/baking-mesh-skeleton-poses-in-godot-4-4/68bdc7c86e4f300001dd2ee9Tue, 11 Mar 2025 15:35:57 GMT

We're almost done with developing the environmental hazards and this last one proved a bit more difficult than anticipated. Thankfully, Godot 4.4 released a few days prior to starting the work on this [great timing], which had some additions that helped overcome the main issue.

The hazard being developed is the electric floor tiles, which if the player steps on will electrocute them. The original idea was to have a bit of fun with this one, and rather than have a generic damage effect applied to the player, instead freeze them in place and have them crumble into a pile of ashes.

As the player would be charred into a single colour, achieving the visual effect of the ashes falling could easily be achieved with a basic particle emitter; the challenge was getting those particles to spawn in a shape that represented the player.

Thankfully, Godot supports being able to create complex emission shapes (see This Page for more information), so it seemed this would be a rather easy task. Unfortunately, when using the method outlined in the documentation, the emission points it generates don't follow the surface of the deformed mesh (i.e. the way it is posed using a Skeleton3D). Instead, the generated emission points follow the surface of the mesh in its unposed state, like you can see here where the arms are posed upwards, but the emission points are generated as if the arm is by the player's side (i.e. how it is in its default pose):

Baking Mesh Skeleton Poses in Godot 4.4

Had the poses been made in Blender, it would have been possible to export meshes in those poses, but the posing was done purely in Godot, so a solution to bake the pose into a new mesh within Godot was required.

Luckily, Godot 4.4 had just been released and within it was a new addition (see PR #85018) that provides this specific functionality! Huge shoutout to smix8 and the Godot team that tested and helped it get into this release; you are the MVPs.

Baking Mesh Skeleton Poses in Godot 4.4

There was unfortunately still one minor issue - the bake_mesh_from_current_skeleton_pose function isn't exposed in the GUI at all, and although this could be done at runtime theoretically, it would be runtime overhead that isn't necessary, given that in the case of this animation - we know what we want to be baked ahead of time.

Thankfully, it is pretty easy to create tool scripts in Godot, and so the functionality was implemented like this:

@tool
class_name MeshBaker
extends Node3D

@export_tool_button("Bake", "Callable") var bake_action: Callable = bake

@export_group("Settings")
@export var applied_scale: Vector3 = Vector3(1.0, 1.0, 1.0)
@export var source_meshes: Array[MeshInstance3D]


func bake() -> void:
	for mesh in source_meshes:
		var baked_mesh := ArrayMesh.new()
		mesh.bake_mesh_from_current_skeleton_pose(baked_mesh)

		var baked_name := mesh.name + "_Bake"
		var existing_node: Node = find_child(baked_name)

		if existing_node:
			remove_child(existing_node)

		var mesh_instance := MeshInstance3D.new()
		mesh_instance.name = baked_name
		mesh_instance.mesh = baked_mesh
		mesh_instance.scale = applied_scale
		mesh_instance.skeleton = NodePath("")

		add_child(mesh_instance)
		mesh_instance.owner = get_tree().edited_scene_root

The script allows for multiple meshes to be passed to it in the GUI so they can be baked and have the specified scale applied to them after clicking the "Bake" button. In this instance, the player consists of 6 separate meshes, so all 6 of these were assigned to the MeshBaker node like so:

Baking Mesh Skeleton Poses in Godot 4.4

After clicking the bake button, we now have a baked copy of the posed meshes and they all appear as children of the MeshBaker node:

Baking Mesh Skeleton Poses in Godot 4.4
Baking Mesh Skeleton Poses in Godot 4.4

If anyone else is in the same boat of having their animations / poses exist exclusively in Godot, feel free to use this script in your own projects; hopefully it can help.

Although there isn't much that can be added to the script, any future versions / improvements of the script will be available on GitHub at https://github.com/FiveNineGames/godot-mesh-baker 🙂

]]>
<![CDATA[Developers Hate It! See How This Door Caused Hours of Debugging with One Simple Trick]]>A notable amount of bugs have been uncovered in the last week and caused some notable set backs (thankfully all fixed now). One particularly painful bug though revolved around this block and how it mysteriously would cause all NPCs to freeze in place:

A screenshot of the block that revealed the bug

This particular level requires a user to

]]>
https://blog.fiveninegames.io/developers-hate-it-see-how-this-door-caused-hours-of-debugging-with-one-simple-trick/68bdc7c86e4f300001dd2ee8Sat, 01 Mar 2025 22:53:39 GMT

A notable amount of bugs have been uncovered in the last week and caused some notable set backs (thankfully all fixed now). One particularly painful bug though revolved around this block and how it mysteriously would cause all NPCs to freeze in place:

Developers Hate It! See How This Door Caused Hours of Debugging with One Simple Trick

This particular level requires a user to unlock a door before being able to progress into the second half of the map. Upon entering that second half, there are two ways to progress further and both require destroying an obstacle:

Developers Hate It! See How This Door Caused Hours of Debugging with One Simple Trick

Once either of those obstacles are destroyed, it results in the navigation map that the enemies use to move around the level to be rebaked. As soon as this happens, they would freeze in place unable to move.

Little did I know, I had seen the issue earlier in the day, and didn't connect the two things. Earlier, the enemies began to sink into the floor, which seemed strange. I "solved" this by altering the calculated velocity so that the Y value always remained at 0 (given the levels are all flat), like this:

var next_path_position: Vector3 = navigation_agent.get_next_path_position()
next_path_position.y = global_position.y
var new_velocity: Vector3 = global_position.direction_to(next_path_position) * movement_speed

Problem solved! Or so I thought; in reality, all this did was create the new bug, which would make the NPCs freeze in place.

The first thing to figure out was why they were freezing in place. As I hadn't connected the change I had made earlier to the current issue, it wasn't initially clear, but the reason they were stuck in place was because the next position being returned by the navigation agent (using get_next_path_position()) was directly below the current position of the NPCs, but as the Y velocity was being reset to 0, they were never moving down and reaching the next point.

After realising this was all down to the bug I thought I had already fixed, the question remained... why on Earth is this happening!?

Developers Hate It! See How This Door Caused Hours of Debugging with One Simple Trick

When testing in the first half of the map, it was outright not possible to replicate the problem. The only thing that would cause the NPCs to start trying to move beneath the ground level, was destroying one of those two obstacles in the second half of the level.

From a code perspective, it made no sense. There was nothing unique about those obstacles. Nor should them disappearing affect the navigation map in a way that would give it extra height.

After hours of testing and debugging, I realised what was unique about destroying those two obstacles compared to all the others - this door!

Developers Hate It! See How This Door Caused Hours of Debugging with One Simple Trick

The player has to open this door to get to the second half of the map. When it opens, it sinks into the ground. When it does this, it doesn't result in the navigation map being rebaked, as there is no need for it; or so I thought...

As It turned out, the collision layer of the physics body for the door was incorrectly set resulting in the door being included when baking the navigation map. This meant that the player would open the door, moving the physics body mostly below the ground level and then the next time the player destroys an obstacle and forces the navigation map to be rebaked, it would do it with the door's physics body below the ground.

Why exactly this caused NPCs on every point of the map to start trying to move below the ground level? I can't say, but it was changing the height of the map being calculated by Godot.

All those hours of debugging were ultimately fixed by changing the collision layer... i.e. changing a number from 1 to 18...

Developers Hate It! See How This Door Caused Hours of Debugging with One Simple Trick

The lesson to take from this? Make sure to carefully consider which objects are in your navigation regions, and if trying to keep a completely flat navigation map, make sure anything moving below the ground level is not included in future bakes of the navigation map.

]]>
<![CDATA[We Have Flamethrowers [Almost]]]>This week it's been back to creating things to give players a bit more of a chaotic experience once they've got to grips with the core mechanics of the game. This week's selection of environmental hazards? Fire!

The flame throwers are wall mounted and

]]>
https://blog.fiveninegames.io/we-have-flamethrowers-almost/68bdc7c86e4f300001dd2ee7Thu, 20 Feb 2025 20:18:30 GMT

This week it's been back to creating things to give players a bit more of a chaotic experience once they've got to grips with the core mechanics of the game. This week's selection of environmental hazards? Fire!

We Have Flamethrowers [Almost]

The flame throwers are wall mounted and activated by the player coming within semi-close proximity of them. To give a visual indicator to the player that the flame throwers are about to start firing at them, they would need to animate out of the wall as if to arm themselves.

I decided to share this in a progress update as the flame thrower is a pretty good example of how a few primitive shapes can be transformed into something more thematic and visually appealing with an albedo texture and a normal map.

Asset creation can be a rather demotivating task at times; particularly when spending a lot of time and effort and it not looking how you want it to look. I thought I'd show how this progressed as an example of why it's worth just taking it a step at a time and not focusing in on it not looking great right away.

Initially, the flame thrower was effectively a narrow cube and five cylinders stacked on top of each other - not something that looks like it would fit in the game and not really clear what it is.

We Have Flamethrowers [Almost]

After creating a basic texture in Inkscape to add some bolt / mounting points to the base plate, some inlets / vents to two of the cylinders and darkening some areas to try and portray some depth, it began to take shape a bit more and it at least resembled more of what it is supposed to be:

We Have Flamethrowers [Almost]

Given the camera angle of the game, this would probably have been more than enough detail, but adding a bump map / normal map makes some of the details stand out much more:

We Have Flamethrowers [Almost]

After [quite a lot] more work in Godot, the arm and disarm animations are done and it now chucks out a nice warm flame towards the player 🙂

0:00
/0:18

]]>
<![CDATA[Typing My Way Out of GDScript Trouble]]>I had been aiming for a few more game mechanics to be complete this week, but what started out as tidying up a small chunk of code has led to several days of bug hunting and refactoring.

Although the majority of the code base uses Static Typing, there was quite

]]>
https://blog.fiveninegames.io/typing-my-way-out-of-gdscript-trouble/68bdc7c86e4f300001dd2ee6Mon, 10 Feb 2025 18:00:52 GMT

I had been aiming for a few more game mechanics to be complete this week, but what started out as tidying up a small chunk of code has led to several days of bug hunting and refactoring.

Although the majority of the code base uses Static Typing, there was quite a lot of code that was carried over from prototyping into production that was a bit more loose with how it handled things.

The thing that initiated the mass refactoring was removing a function that I thought was no longer needed (as no references where found when using the Find All References function in VSCode), only to find it was still in use. As the instance of the function being called was not on a statically typed object, it was never picked up and ultimately led to a runtime error waiting to be found.

The place this happened was in a for * in loop and upon reviewing more of these loops, I began realising that I had no idea what the data type was that I was dealing with in several of them.

I also came to realise that any loops where I was checking types was hugely repetitive and basically just boiler plate code, i.e.:

for node in $NamedNode.get_children():
  if node is SomeType:
    do_whatever_is_needed(node)

or:

for node in $NamedNode.get_children():
  var n := node as SomeType
  if n:
    do_whatever_is_needed(n)

I decided I'd replace instances of this with a helper function I created (for_each_child) to loop through a specified node's children, do the type checking, and then fire the callback:

class_name Globals
extends Node


static func script_is_type_of(script: Script, type: String) -> bool:
	if script.get_global_name() == type:
		return true

	var base_script: Script = script.get_base_script()

	if base_script:
		return script_is_type_of(base_script, type)
	else:
		return false


static func for_each_child(obj: Node, node_type: String, callback: Callable) -> void:
	for child in obj.get_children():
		if child.is_class(node_type):
			callback.call(child)
			continue

		var script: Script = child.get_script()
		if script and script_is_type_of(script, node_type):
			callback.call(child)

Using this function changes the first code sample to read like this instead:

Globals.for_each_child($NamedNode, "SomeType", do_whatever_is_needed)

Alternatively, if I wanted something inline rather than to pass the child node to another function, I could use a lambda:

Globals.for_each_child(
  $NamedNode,
  "SomeType",
  func (node: SomeType) -> void:
    do_the_thing(node)
    do_another_thing(node)
)

It can create more lines of code when using lambdas for inline blocks, but it has helped a lot when refactoring blocks like this to now be able to see clearly what types I am working with.

Additionally, I enabled some extra warnings (found in Project Settings > Debug > GDScript) to make it easier to notice instances of operations that may not be type safe; namely:

  • Unsafe Property Access: produces a warning or an error respectively when accessing a property whose presence is not guaranteed at compile-time in the class.
  • Unsafe Method Access: produces a warning or an error respectively when calling a method whose presence is not guaranteed at compile-time in the class.
  • Unsafe Cast: produces a warning or an error respectively when a Variant value is cast to a non-Variant.
  • Unsafe Call Argument: produces a warning or an error respectively when using an expression whose type may not be compatible with the function parameter expected.
  • Untyped Declaration: produces a warning or an error respectively when a variable or parameter has no static type, or if a function has no static return type.

These warnings unfortunately can't fully detect context, so even if something is guarded with a if obj is ExpectedType statement, it will treat obj as whatever it's declared type is and warn accordingly.

If you're working on something with a notable amount of code, I'd personally recommend enabling these settings and ensuring nothing is left untyped; it was a tough week fixing up all the warnings after the fact!

]]>
<![CDATA[Slamming Doors and Starting Devlogging]]>Welcome to the first [official] devlog! I've been posting updates between Threads and Bluesky but decided it would be better to create a dedicated devlog where I can get into more detail rather than worrying about post size limits.

With a lot of the tooling and core functionality

]]>
https://blog.fiveninegames.io/slamming-doors-and-starting-devlogging/68bdc7c86e4f300001dd2ee5Mon, 03 Feb 2025 23:55:13 GMT

Welcome to the first [official] devlog! I've been posting updates between Threads and Bluesky but decided it would be better to create a dedicated devlog where I can get into more detail rather than worrying about post size limits.

With a lot of the tooling and core functionality of the game complete, I started work on the first environmental hazard of the game; the slamming doors. In true indie fashion, I under estimated the challenges this would actually introduce. I initially expected to be done with them in 1-2 hours but spent a good 8 hours modelling and coding in the end.

They work much like the name suggests - the doors open and shut automatically, if you get caught in them when they are slamming shut... you'll start to resemble Flat Stanley (understanding that reference will probably show your age).

The first of the main two hurdles to overcome was one that has been a constant issue when designing mechanics for this game, given the top-down view - not being able to abuse clipping to hide things. This meant the doors sliding open couldn't just hide within the walls it is attached to, instead the doors needed to scale down with the origin point being where they attach to the frame.

The other hurdle was dealing with the squashing / respawning routine. Setting up a respawn point in itself wasn't a huge deal. As you can see in the screenshot below - I created an item that can be added to the map to signify where the player should respawn and that was that; the real problem came with how the respawn process is carried out.

Slamming Doors and Starting Devlogging

When the player is squashed, the scale property is used to achieve the visual effect, however, some equippable items are children of the mesh that is scaled. The reason this was a problem is because, ideally, you should not scale collision shapes in Godot [the game engine being used], as it can cause unexpected behaviour and bugs in physics calculations.

In the end, the method I settled on (which I feel is the cleanest) was to instantiate a new player, copy the relevant properties that need to persist through the respawn process and then move the newly created player to the respawn zone whilst discarding the previous player object.

It took a lot of bug hunting and tweaking, but the doors are now working as expected and the respawn points now also work! 🙂

0:00
/0:07
]]>