Making a non-Euclidean game in Godot

I made a game with non-Euclidean mechanics for "Stop Waiting For Godot" game jam last weekend!

You can check it out on itch.io (this includes a web version) and find the source code on GitHub.

This is a post about how I did it and my first-time experiences with Godot.

Background

Over years I have worked with a variety of game engines, level editors, and other tools, but my prior experience with Godot was primarily limited to the over-engineered April Fools joke I did this year.

Idea

The jam's (optional) theme was "keep it simple", but, as you might know, I don't make things because they're easy.


The Cool Moon's influence is irresistable

It's been quite a while since I made any first-person exploration games and I do quite like portals, perspective tricks, and various other puzzles, so I figured that it'd be cool to make a little game utilizing these.

Portals

Godot's equivalent of "render texture" is ViewportTexture - so you make a viewport with its own camera, it renders to the texture, and then you do what you must with it.

The reasonable approach to portals is that you render what's behind the portal to a texture (potentially a few times if there's a "loop") and show this texture in place of portal's pixels on screen.

The official template index has "2d in 3d" and "3d in 2d" sample projects, but not "3d in 3d", so I decide to cobble a portal from "3d in 3d" and the "using viewport as a texture" tutorial.

While doing so, I at some point tick the "Own world" checkbox on the viewport without thinking - a mistake that cost me a hour of my life:
If there's no world set in the associated "World" property, this causes the viewport to use a blank world - and that blank world doesn't use your default environment settings, so it's pitch black.
Combined with inability to preview what's going on in a viewport without moving the contents to a new scene, it took some trial and error to figure out why nothing is rendering.

With that out of the way, I finally get something to render to the 3d plane in the world space, but the picture looks dark and washed out. With some experimentation (and reading descriptions of every Viewport property this time around), I figure out that:

  • Viewport should have "Keep 3d linear" flag set.
  • Material should have "Unshaded" flag set.
  • If you are using shadows, viewport should have "size" in "Shadow Atlas" set accordingly or the shadows won't show in it.

With that done, I finally have a consistently-looking render texture!

Now, it is time to map the texture to screen space, which implies a shader. I pick the conveniently-shaped "Convert to ShaderMaterial" option in my material's dropdown, it converts, and... it doesn't render in-game anymore!

After some poking around I establish that I found a bug - an important material checkbox gets unticked upon conversion.

With checkbox ticked back on, I replace the UV coordinates in the shader by SCREEN_UV, match the portal's camera to player controller's camera, and just you look at that - a real portal!

Well, except having the exact same POV as player renders the portal invisible, so I add a couple boxes to portal's viewport to make it apparent that it's there at all.

Finally, it is time for actual portal mechanics! I make the "portal" object linkable to another such object, write the most basic code to adjust the player's position as camera gets past the portal plane, and find a number of issues:

  1. I cannot make the portal plane ignore the near culling boundary, meaning that you can peek through a portal by carefully approaching and tilting your camera downwards.
    I proceed to "fix" the issue by adding a backup portal plane behind the usual one - just far enough for the player to not be able to pull this off anymore.
  2. I cannot [re-]render a viewport on demand (proposal), which means no "recursive" portals and also that there can be occasional anomalies related to update order (read: whether everything updates on time between player teleporting and a frame rendering):

    I sync the portals after teleporting the player, but the issue persists.
    I start developing a complex workaround involving a one-frame-late render of the screen for portals to use immediately after teleporting, but... just having a viewport attached to the player and rendering the texture behind their back fixes this for some reason?

    I'm scared to learn why does this even work.

  3. Godot doesn't have clipping planes nor stencil buffers (proposal, discussion) so I cannot efficiently prevent geometry from obstructing the view behind the portal.
    I settle on simply not having anything behind the portals - all of them are facing out of the level, where nothing can get in the way.
  4. I can't really have portals facing different directions because this interferes with already-fragile shadow rendering.
    I try to ditch shadows in favor of SSAO, but it doesn't play well with portals, and I have even less control over it than with directional shadows.

Considering all of the above, most of the Cool Level Ideas that I had in mind are out of the window.

I decide to salvage what I can by introducing a second visual gimmick: ...orb!

The orb is a polyhedron (kindly produced by default settings in Blender) that curiously spins on multiple axes and uses the same portal camera behaviour to project a world through itself, BUT:

I couldn't help but to notice that objects have render layers, and each camera can choose which layers to draw, so I can have objects that are only visible through an orb, or objects that are revealed to be fake by an orb.

My original vision for the orbs involved utilizing per-camera environment settings to have them project a night-time version of the level with hidden objects spotting a starry pattern look, but this plan had bit the dust after I noticed that DirectionalLights don't care about their render layers and shine no matter what, so I had to settle for a simpler-looking purple tint and hidden objects appearing in a moderate pink color.

With all this done, I have enough tools for no less than a few rooms worth of puzzles and less than a day left to make them.

GDScript

Although it was extremely tempting to use Haxe in Godot, I decided to use the included scripting language for once.

GDScript turned out to be pretty good - it's rather Python-like, but has a large number of convenience features and optional typing - I follow similar philosophy with GMEdit (see wiki).

You can write most things that you expect from a modern scripting language without looking them up and have them work, although sometimes not the way you thought - e.g. p = {a: a} will use value a as a key - you'll need p = {a=a} for key to be string a.

Naming convention is quaint - types are PascalCase but fields are snake_case.

There is get_node and find_node, both of which accept a string parameter and return a node, but do very different things.

class_name is good for auto-completion, but is currently a little limited since cyclic references between scripts are forbidden.

Auto-completion occasionally gets lost when asked for something specific:

I also tried the official VSCode extension, but it also had issues with auto-completion,

and had a few other issues such as:

  • Extension getting stuck on connection if the editor doesn't respond, requiring a VSC restart.
  • Extension indenting incorrectly
    (pressing Enter, Backspace, Enter after a function block would falsely re-indent)
  • Godot editor occasionally crashing due to pending bug in non-beta version of Godot.

so I eventually went back to using the "built-in" code editor.

Level design

Having used the editor to hastily block out levels, I have some thoughts:

  • Although you can split a scene editor into two or more views, you cannot open two scene editors side by side (and opening a project in two Godot windows is generally considered risky), so editing a "prefab"/sub-scene means that you have to switch back and forth between them to see how the thing fits in "big picture".
    As result, I dumped most of my geometry directly into the main scene.
  • A combination of not being to scale objects in Select mode and not being able to select objects in Scale mode is a bother.
  • The editor could really use a Unity-style "rect" tool for scaling objects starting from a corner/edge - each time you scale an object, you have to move it later (see above), and it didn't occur to me until well after the jam end that the issue can be partially side-stepped by making object templates not centered and instead starting at XYZ=0 and extending in one octant.
    There is an plugin for box modelling, but it's still in early stages as of writing this.
    My own (post-jam) attempts
  • Not only that, but Scale tool snapping works in percentage multipliers, which meant that it's easier to enter scales by hand in Transform section.
  • Speaking of which, having to expand the Transform section every time you click on a new object is a bother.
  • Entering 1/<integer> in Transform and getting 0 is only funny the first time around.
  • The editor would occasionally hang when using Tab and Shift+Tab to navigate between fields, making the data entry experience a little more annoying.
  • I could not figure out where do you edit the grid size, assuming that you can at all.
  • Snap settings are peculiarly per-scene and the menu acts a little weird (no formula support, can't close by pressing Enter)
  • Not having vertex/surface snap is also a trouble, especially seeing that the editor can snap to surface when dragging and dropping an object to the scene, but won't let me have that effect when I'm moving around an existing object.
    There is also a WIP plugin for this, but I couldn't timely figure out how to actually snap with it even after reading the source code.
  • It took me a little bit to realize that you make a cone by taking a cylinder and shrinking one of its ends to zero.
  • MeshInstance and CollisionShape boxes use different units ("radius" vs "diameter") for some reason.
  • Much to my surprise, live scene reloading actually works >90% of the time - so long as you aren't re-parenting objects or moving something around in a sub-scene (which often disregards parent scene's transforms when applying changes to the game).
  • Node paths are cool but tend to break whenever you move things are the scene tree.
    Wouldn't think I'd ever wish for GUIDs of some sort!

After not having such a good time with level design flow, I decide to check out if Godot has CSG tools or anything - and to my surprise, yes, it does, but the documentation page opens with a rather direct "how about you use something else" message.

I download the suggested Quake map editor, but it's been a hot minute (possibly an entire decade) since I've last used the kind of tool and it wasn't even this one so I decide to play around with it when I'm not on deadline.

Godot CSG turns out to be mostly okay, but sometimes freaks out and doesn't triangulate faces when using semi-complex combinations of add-subtract operations. I wiggle the hole-shapes around the wall until they stop producing seams in the mesh and call it good enough.

Lights and shadows

Where do I even begin here

  • Light and shadow settings are scattered all over the place - some are in light's settings, some are in Project Settings, some are in Environment Settings, some are in Viewport settings.
  • Tweaking some of these settings requires restarting the editor, which subsequently forgets what scenes did I have open.
  • Having relatively small/thin objects causes shadow anomalies;
    Settings intended for fixing these result in other anomalies (to the point of non-shadow-casting objects suddenly gaining half-shadows);
    I eventually settle for much thicker walls as the workaround with least side-effects.
  • SSAO disregards "no shadow" flags on objects, which can only be fixed by marking the material as transparent (even though it isn't) or assigning alpha in the shader.
  • After struggling enough with trying to make shadows look bearable, I decide to bake them with built-in Baked Lightmaps. I subsequently discover that I can't actually do this because none of the primitive shapes that I've been blocking out the levels with have UV2 and the editor does not want to generate UV2s for them because they're not ArrayMeshes.
  • My attempts to play with Baked Lightmaps in a blank project resulted in breaking Godot (A, B)

Physics

Nothing to be particularly proud of, to be honest - player physics are pretty much just the code from First Person Exploration template, except I tweaked the physics to be a little "snappier" and added "coyote time" (you can press jump a split-second late after falling off a block).

Portal physics are a little more complex, but ultimately come down to coordinate adjustments.

The single issue I encountered is that standing on two overlapping colliders causes the player to "vibrate" due to move_and_slide pushing them out... for some reason.

HTML5

Sometime halfway through game development I figured that it's finally time to run my game on HTML5 and see if it works at all or not.

Despite (the manual mysteriously saying "If a runnable web export template is available" (without linking to a page that notes that you need to download export templates first), everything went better than expected.

The good news are that Godot uses WebAssembly, meaning that games work pretty close to native on HTML5, save for having to lock mouse in response to a click and some vendor-specific oddities with complex shaders.

The bad news are also that Godot uses WebAssembly, meaning that your little game comes with a big 17MB wasm blob (4.4MB zipped).

Optimization

With jam deadline extended, I decide to actually have someone play-test my game.

After giving a build to the first person with an integrated GPU, I discover that:

The first one is an easy fix - I can add a key to toggle shadows.

The second is trickier - I try VisibilityNotifier, but this doesn't help much as this includes portals occluded by walls; adding a distance check only helps so much as many portals are only visible from a specific narrow corridor.

I settle on a different approach - since I know very well where you are supposed to look at portals from, I can set up 1-2 regions around the portal that the player needs to be in for the portal to be visible when it's expected to cross the player's view.

This also allows me to switch orbs to a non-portal material as soon as there's no reason to care about them.

With all that done, the game seems to run well enough.

Community

Since a game jam implies a deadline, I decide to ask questions on the official Discord server.

Initially this has mixed results - most of my questions are less common than I think them to be, and the server does not employ a "busy help channel" system like GameMaker server does, so plenty of time someone else asks a question after me and my message drifts out of view, never to be seen again:

I eventually start asking questions in the channel dedicated to the game jam, and this works out better, as there's at least one other person (eons) that worked with Viewports before.

The answer to some of my questions turns out to be "you cannot, but this is planned for Godot 4".

Godot 4 is an upcoming, almost-mystical major version that is set to address many of the existing issues with the engine - a holy grail of sorts. Naturally, with so much to do, no one knows precisely when Godot 4 will be ready.

Aftermath

In past few days I have decided to experiment with Godot a little more, determined to squeeze some adequate-looking lighting out of it after all.

I look into TrenchBroom a little more.

It is good, but lacks support for concave geometry, which is essential if you don't want your composite shapes (say, a wall with a doorway cut through it) to do this:


(the seam appears at the shared edge between two pieces of geometry)

I was originally sceptical of being able to achieve seamless baked lighting across portals, but what do you know - perhaps I can't have seamless lighting at all! If lighting is glitchy everywhere, would this make the player less suspicious of portals?

My attempt to make an editor plugin to automate the process of re-importing and re-generating the lightmap as I fiddle around with materials and settings resulted in immediately breaking Godot in two different ways.

Time will show whether I'll further revisit this - I suspect that the answer to many of my exotic problems lies in tweaking the engine to behave as necessary, but the games I want to make aren't nearly big enough to justify doing so.

Conclusions

  • You can totally make a videogame in Godot, perhaps even a less-common kind.
  • I couldn't get the visual mechanics to work to an extent desired, but this did lead me to coming up with new, less common mechanics.
  • I wasn't able to achieve the intended visual aesthetic, but there's always next time.
  • Designing 3d level geometry in Godot's scene editor is not such a good idea.
  • I remain to have peculiar luck with software.

Related posts:

2 thoughts on “Making a non-Euclidean game in Godot

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.