Figment Game Studios

July Studio Update: Becoming Godot Native

FIGMENT

Why we moved our first game's runtime from TypeScript to GDScript, how we managed the migration, and what becoming Godot-native unlocks.

A progress screenshot of the Figment Game Studios systems sandbox.

Game Progress

In May, I wrote about why I had chosen TypeScript for our first game.

At the time, it was the right decision. TypeScript is the language I know best, and building the game in Godot through godot-js gave me a familiar way to approach a very unfamiliar problem. It let me bring years of software development experience into a new medium without having to learn every part of the engine and a new language at the same time.

Two months later, I have moved the entire project-owned runtime to GDScript.

That might sound like a dramatic reversal. In practice, it is the result of the same thinking that led me to TypeScript in the first place: choose the tools that give the project the strongest foundation, and be willing to revisit the decision when the evidence changes.

The TypeScript experiment did not fail. It helped build a playable prototype, shaped the architecture, and gave me the confidence to develop systems I had never built before. But the project has changed since that original decision, and the balance of benefits has changed with it.

Why change?

The first pressure was the engine itself.

Using TypeScript meant relying on a community integration between the language and Godot. That integration was tied to Godot 4.5 while the engine continued moving forward. Godot 4.6 and 4.7 introduced improvements I could not use, and more substantial changes are already being developed for 4.8.

There is nothing unusual about a third-party integration taking time to catch up with a fast-moving engine. The problem was that the version of Godot I could use was no longer a decision I controlled. For something as fundamental as the game’s runtime, that had become too important a constraint to ignore.

The second pressure was architectural.

The project was no longer purely TypeScript. Some engine-facing work needed to be written in GDScript, which meant the runtime had gradually become a mixture of both languages. Values crossed the boundary through dynamic method calls and, in some cases, JSON. Generated engine typings, patched dependencies, Node, pnpm, Vitest, and the GodotJS toolchain all existed to support that boundary.

Each individual piece was manageable. Taken together, they created friction in exactly the place where I wanted the project to feel most direct: between the code, the scene tree, and the running game.

The final pressure was my own development process.

When I began, choosing my strongest language reduced the amount I needed to learn at once. Since then, I have become much more comfortable with GDScript and with Godot itself. The implementation process has also become heavily AI-assisted. I still own the architecture, behaviour, review, testing, and final decisions, but preserving my preferred syntax matters less when much of the code is being produced collaboratively.

What matters more now is being able to understand and work with the game inside Godot: following signals, inspecting exported values, navigating scene relationships, debugging native values, and keeping the number of tools involved as small as practical.

GDScript is not my favourite language in isolation. It does not need to be. In the context of this project, its relationship with Godot is more valuable than my personal preference for TypeScript syntax.

Migrating without rebuilding the game

By the time I made the decision, this was not a small prototype with a handful of scripts. The project-owned runtime contained 110 TypeScript files alongside 31 GDScript files. It already covered application settings, player and NPC movement, inventory, persistence, weapons, attachments, combat, collision line of sight, multi-floor visibility, UI, diagnostics, and the systems sandbox that ties them together.

A rewrite would have been reckless.

Instead, I treated the work as a staged migration. The first step was an architectural decision record that made the destination and the rules explicit. The language could change, but settled gameplay and system decisions could not quietly change with it.

The migration then moved through the game in small vertical slices:

  • establish native GDScript tests for a piece of behaviour;
  • port the owning domain rather than mechanically translating individual files;
  • switch the real runtime caller to the native implementation;
  • verify behaviour in tests and in the running game;
  • remove the replaced TypeScript authority in the same step;
  • commit that slice before beginning the next one.

That last part was important. At no point did I want two long-lived implementations of the same behaviour, with nobody quite sure which one was authoritative.

I also resisted turning every TypeScript file into a GDScript file. Some of the old modules were meaningful boundaries that hid complex policy. Others existed mainly to carry information across the language integration. The former deserved to survive; the latter deserved to disappear.

The engine upgrade was deliberately kept separate. The behavioural migration stayed on Godot 4.5.1 so that a regression was likely to come from one source. Only after the active runtime no longer depended on GodotJS did I remove the TypeScript toolchain and move the project to Godot 4.7.1.

It was not painless

A migration of this size was always going to uncover problems.

At different points, a couple of NPCs disappeared, performance dropped sharply, and the floors above both stairwell stacks began rendering when they should have been hidden. Those were not theoretical test failures. They were visible changes in the game that had to be investigated and corrected before the migration could continue.

That is why the work moved in slices and why playtesting remained part of every stage. Automated tests are excellent at protecting rules and contracts, but they do not replace looking at the game. A test can confirm that a visibility result contains the expected data; a playtest reveals that the stairwell above the player is still being drawn.

Performance was especially useful as a reminder that behavioural equivalence is wider than producing the same answer. When frame rates fell, the cause was not NPC decision making, as I first suspected, but work happening around collision and floor visibility. The migration had changed when some calculations occurred, even though the final visible state was intended to be the same.

Those regressions were fixed, and the final migration passed the full native validation suite. Performance still varies across the sandbox, and a dedicated optimisation pass will happen later. The goal here was not to claim that the game is finished or perfectly optimised. It was to make sure the migration did not leave a known behavioural or architectural regression behind.

Where the project is now

The mixed runtime of 141 TypeScript and GDScript files has become 123 project-owned GDScript files. The lower number is intentional: shallow translation layers were absorbed rather than recreated.

The native test foundation now includes 67 unit test files and 28 runtime test files. The final clean-checkout validation imported the project, passed all 33 registered check groups, booted the application, entered gameplay, and completed an 11-stage debug tour covering the major systems.

The repository no longer needs GodotJS, Node, pnpm, generated TypeScript definitions, patched JavaScript dependencies, Vitest, or the machinery that connected those pieces to Godot. More importantly, there is no remaining TypeScript implementation sitting beside the GDScript runtime as an accidental second source of truth.

The game is now running natively on Godot 4.7.1.

That gives me a cleaner repository, a simpler toolchain, better visibility inside the Godot editor, and control over when the project adopts future stable engine releases. It also gives the next phase of development a much clearer starting point. Future optimisation and architectural work can focus on the game itself rather than carrying a language migration behind it.

Was TypeScript the wrong choice?

No.

It was the right choice for the version of the project that existed in May. It gave me a productive starting point, helped establish strong boundaries and tests, and reduced the number of unfamiliar things I had to solve simultaneously.

GDScript is the right choice for the project that exists now.

Software decisions are not promises to defend forever. They are decisions made from the best information available at the time. A healthy project should be able to recognise when its assumptions have changed, preserve what still works, and replace what no longer serves it.

That is what this migration was about.

The architecture, tests, and systems thinking developed during the TypeScript phase have not been discarded. They have been carried forward into a runtime that fits the engine more naturally and gives the project more freedom to grow.

After a very large amount of careful, unglamorous work, our first game is now fully Godot-native. I am relieved, a little exhausted, and very happy to be on the other side of it.

What I didn’t mention

You probably noticed that the screenshot in this post looks rather different from the one in our May update. There’s a story behind that too, but it deserves a post of its own. Stay tuned if you’re interested in how and why Game A became Game B.