Music
Made by request made with love

I push buttons, and I make free + open source virtual reality games

Postmortem - Crystal Bastion

Table of Contents

About

Crystal Bastion was a VR game made in 7 days for the September 2025 Godot XR Game Jam. The theme of the jam was "It Takes Two".

About the game

This game is a simple "defend the base" game - waves of enemies walk to the player's base and try to destroy it, and the player must defend it.

The player controls two character rigs, one with a laser gun and one with a laser sword. The player can switch between the two, with a brief anti-spam cooldown.

The player's 2 weapon types are both rigidbodies, and grab interactions with these weapons are done with Godot's Generic6DOFJoint3Ds. This means that unlike with the approach used for the Godot XRTools addon, the player is unable to pick up a weapon and freely swing it through walls*.

You can see this system in action below. The blue hand is where my controller is, which is not visible in the final build.

* Off the top of my head, I'm pretty sure that held pickables will collide with RigidBody3Ds, but will clip through any StaticBody3Ds.

Goals for this project

The main goal of this game was to try out the physics grab interaction system that I've been working on. At the moment, the feature list of the system is as follows:

  • The player can grab physics pickables, both ones right at the player's hand, and from a distance
  • The pickable will be grabbed at the most appropriate GrabPoint. The pickable will align to this GrabPoint when grabbed
    • By most appropriate, this means the closest grab point (position + angle), that the HandGrabPoint's handedness matches the grabbing hand's handedness
    • This is code pulled from Godot XR Tools' FunctionPickup.gd
  • When picking a pickable up, the pickable will smoothly lerp to the player's hand before the physics joint is set up

I'm currently smoothing out the edges of 2 handed interactions - you'll probably see that implementation in the next game jam.

The secondary goal for this jam was to meet the theme requirements of the jam. I was close to making a multiplayer game (thinking something along the lines of a VR + Desktop PC multiplayer game, where the desktop player would control a small companion mech like in Metal Gear Solid 4) - but I decided to lock in on grab interactions instead. The end result was a system where the player controls 2 characters, each with a different weapon type.

Technical Details

Physics grab interactions

How it works

The node breakdown of the hand joint prefab can be found below. 2 of these are spawned for each player rig (one for each hand) and they're spawned as a child of the level itself. Spawning the hand joints separate from the player rig is done in an attempt to avoid any weird physics jank that may occur with nesting rigidbodies.

As a root node, I have a Generic6DOF3DJoint which binds the Anchor RigidBody3D, as well as the held pickable. The Anchor rigidbody does not move at all - it remains kinematic and locked in at local position/rotations of Vector3.ZERO.

The PickableGrabBag contains all of the "selection" logic: identifying when an object is hovered by the hand, either via overlap radius or via a ranged grab. This script is a stripped down version of Godot XRTools' FunctionPickup.gd, refactored into a separate file to isolate selection logic from joint management logic.

The node hierarchy for the physics grab prefab.

Under the hood, the joint is doing all of the heavy lifting. When the player presses the grip button, the pickup routine begins. The pickable is lerped towards the joint over a short duration. At the end of the lerp, I initialize the joint by setting the references to both connected bodies (the Anchor and the held object).

In terms of actually setting up the joint, I've found the secret sauce is that the initial orientation of the two connecting nodes relative to the joint is important. Coming from Unity this was something I had to stumble into, since physics joint documentation isn't well defined - in Unity, you can manually set the RigidBody's position/rotation offset from the joint. This is done automatically in Godot, when you assign the references to the joint's connected bodies (Generic6DOF3DJoint.node_a and Generic6DOF3DJoint.node_b).

On the note of joint alignment, there's a 2 of configuration that I've set up here. On the pickable prefab, I've extended Godot XR Tools' XRToolsGrabPointHand. This is the usual approach that aligns a hand to the pickable, which you'll find in Godot XR Tools' implementation. I've also added a second layer of optional rotational offset, so that I can further rotate the whole pickable from this grab point. I found that the default orientation of the sword after configuring the correct hand pose felt wrong - the sword was being held too far forward. I've seen that some VR shooter games offer a similar offset in their settings, in the event that the pitch of the held gun feels wrong for their controllers.

You can find the source code for all of this here.

Limitations

1 handed interactions

Since it's a simple system so far, there's a handful of caveats associated with the grab system. The main one being that the player is limited to 1 handed grabs. I found I was really limited by this - there were a lot of cool ideas for weapons I had in mind, which would've been nice to work into this jam. Aligning the rotation of the 2 handed grab joints has been causing me a little issue, so I decided to refactor that out of my framework for now.

Joint Configuration, and applying torque

Another issue I was running into was with tuning the spring stiffness of the 6DOF Joints to a decent point. I've set up a catch-all joint configuration that felt good, with an aggressive linear and angular spring joint stiffness.

The downside of this is that I found I was unable to apply a one-shot torque to my pickables, and have it actually look like anything was happening. At one point I tried to apply a recoil to the laser gun (dream with me, that it makes sense for a laser gun to have recoil - it looks cool!), but there was so much correctional torque being applied to the gun via the grab joint that it wasn't visible. Turning the angular spring joint stiffness down resolved this issue, but the grab interaction felt too "floppy". Spending a little more time fine-tuning the joint config should resolve this, and worst-case I could also take a more-gross approach of applying force over a short period of time (which would give me better control over recoil anyways).

The configuration for this joint. The linear/angular spring stiffness+damping were the only real things I needed to play with.

Pickable Center of Mass

On the topic of joint configuration - I've got a hacky workaround for handling the center of mass for my pickables. The issue is in that the pickable's center of mass is different from the hand grab joint's connection point. When I move the player's body RigidBody around for locomotion, Godot applies a torque to the pickable.

You can see what this looks like in the video below - I'm not moving my right hand and all during this clip:

With a default center of mass, the held object has a torque applied to it when the player moves.

This torque appears to be applied along the axis defined by the cross product between the player body RigidBody's velocity, and the direction from the hand joint to the RigidBody's center of mass. I have not confirmed this, and instead opted for a hacky workaround.

By default, the RigidBody3D's center of mass is auto-calculated based on the positional average of all of the RigidBody's CollisionShape3Ds. This can be overridden optionally - RigidBody3D.center_of_mass_mode can be set to CENTER_OF_MASS_MODE_CUSTOM so that the public Vector3 RigidBody.center_of_mass is used instead. Instead of applying the proper counter-torque here, I instead just set the pickable's center_of_mass to the position of the hand joint, functionally eliminating all movement torque! This works for my use-case, but it feels wrong that pickables are perfectly balanced along the grab point. In other words, this is fine for a laser sword or a pistol, but it would feel very wrong for something like a big hammer.

Editor Physics Configuration

One thing worth mentioning is the editor physics configuration. I've set Godot to use Jolt physics, because I've had better results with it in general as opposed to the old physics engine. Less clipping through objects, and the physics simulation feels more accurate to me.

The way that I set up the player rig also caused some physics issues for me. I was finding that while the player rig was in motion, I was getting some jittery movement behaviour on the held pickable - as if the motion of the joint RigidBody3D wasn't being calculated at the same tick rate. I was able to resolve this by disabling Physics Interpolation and setting Physics Jitter Fix to 1.0. Disabling Physics Interpolation hasn't caused any issues for me yet, but that may be because my physics tick rate is set close to what the screen refresh rate is.

The project's physics configuration.

The hand joint prefab spawns as a child of the scene rather than as a child of the parent rigidbody, to prevent any weird interactions with childed rigidbodies. However, I am moving the hand joint to the player's controllers every physics frame, which may have caused some weird physics jank here? In any case, the jitter fix solution worked for me, so I won't be making changes here unless necessary.

Player swapping

At a high level, player rig swapping was done by simply disabling one rig and enabling another. The disabled rig has its Node3D.process_mode set to Disable, and its RigidBody set to kinematic freeze. Conversely, the enabled rig has its process_mode restored to inherit. and its root RigidBody3D unfrozen. I also register current rig as the current one, for tracking via Globals.xr_rig, and also for whatever goes on under the hood of XRTools (enabling the camera, and probably for some form of XRRig.get_current_rig()).

For managing the rig instantiation, I have a simple controller which spawns player rigs. I specify a list of Marker3Ds as spawn points, and an enum to indicate what weapon the player will spawn with. This spawn location is also re-used to respawn the player when they die. I'm pretty sure that at one point I had a different player rig spawning for each weapon type, mainly so that each character could have finer-tuned configurations (move speed, jump height, etc), but I didn't end up taking advantage of that system.

Enemies

Enemies use the same framework that they have for the last few games - motion is driven by RigidBody3Ds, and logic is driven by a FiniteStateMachine. The main change here since last time is that I'm using RigidBody3Ds for moving characters instead of CharacterBody3Ds. There were 2 enemy types in game - the Crystal Ball enemies, and the Suit enemies. The Suit enemies are not mechanically different from my usual base enemy templates, so I won't go into detail here.

The Crystal Ball enemies are meant to give the player some variance when attacking with a sword. Their design is based on the sword fighting minigames from Wii Sports Resort - they would hold their swords either vertically or horizontally, and so the player would have to swing their sword to match that orientation to score a hit. The crystal ball enemies spawn a random number of shields which rotate around it, randomly picking between rotating along the local Y axis or along the local X axis. These shields are simple static RigidBody3Ds. so they block sword hits if the player swings their sword the wrong way.

Each enemy had 2 variants: a blind variant, and a stronger variant. The blind variants had grey retexturing on the crystal, and the stronger variants had a gold retexturing on the crystal. The blind variants were meant to ease the player into the combat gameplay, letting the player observe the enemies without immediate danger. These blind enemies had their VisionComponent marked as blind, so they could not enter the "chase player" state, and would simply chase the player's base instead. The strong variant of enemies simply had more health - I believe they also chase the player faster.

Arena management

Enemy spawning is handled by the ArenaManager. The ArenaManager is responsible solely for the "spawn enemies" routine, which can be started and stopped via global signals. These global signals ended up being super handy for having a loose coupling between Arena start/stop functions and all of the places that should control the arena state (ie: arena UI, the player dying).

The configuration of each round is stored in a resource which stores a list of WaveConfigs. Each wave is defined as a group of a single enemy type being spawned at a spawn location, with some extra information stored about wave timing (ie: how long to wait before the wave starts, how long to wait between spawning each enemy, whether or not all enemies must be killed before this wave is considered complete). Once a round is started, the ArenaManager iterates over each WaveConfig in the round, waiting for various timeouts and instantiating enemies as per the wave config.

The resource used to store a round of combat. Each wave represents a single enemy type spawned at a single location, a given number of times. There's also some extra information that controls spawn timing, and BGM intensity

Reflection

Physics interactions

I'm really happy with how physics interactions feel in this game! I've been sorely missing HurricaneVR after switching to Godot, and I'm really glad to have a replacement in the works. Grab interactions feel fluid, and I haven't run into any weird physics jank yet.

Doing this game jam was really nice, because I got a ton of dogfood feedback about what the framework needs at this point, and about some of the pain points I ran into. Minor things like needing fine designer control over the held orientation of the held object in addition to the hand pose alignment, but also with the little issues that caused the physics engine to jitter and freak out. It was nice working on expanding my framework in this game jam too - pickable holsters weren't something that I designed for in the original spec, but it was something I was able to hook up with relative ease later on (with some minor alignment issues I never had to resolve, and decided to design around instead).

I think going forward, I need to swap to the factory pattern for instantiating grab joints. Having a more generic approach to setting up grabs would be really nice - I'm currently running into this as an issue when programming secondary grabs (storing references to the grabbers on the grabbed object gets weird when you have 2-handed grabs and let go with one). I don't think GodotXRTools supports this as expected out of the box *.

* The example I have in mind is using both hands to grab a shotgun. If you let go of the handle, the opposite hand should remain on the barrel of the gun. Last time I tested things, the only functionality that came out of the box was to swap the gun so that you're suddenly holding the handle of the gun with the hand that was previously holding the barrel of the gun.

Yapping aside, these are the things I want to look into at some point for this grab framework:

  • Sockets need a standardized system. At the very least, I need to clean up my current system
  • Need to fix the center_of_mass issue by applying a counter-torque on player movement
  • Add hand pose support
  • Finish adding 2-handed grab support

Player Rigs

With minor pains, I was able to get my XR architecture working for multiple XR rigs. This mainly meant decoupling global XR things from xr_rig.gd (handling XR controller input to be accessed globally, and accounting for the fact that Globals.xr_rig can both swap mid game and be null).

Using a RigidBody3D character controler continued to be good, after using it for Hellrot (2025). This is a stripped down version of the rig I used in that game, mainly that I don't have a second FSM for the grappling hook. As a result, the core movement approach of "laterally accelerate the rigidbody, and clamp its XZ velocity to some maximum point" worked really well here. I do think that I tuned the player to move too fast - I pushed those values a bit too high because I forgot to whitebox test the environment for scale before designing it, so traversing the world took too long otherwise.

On that note, the most consistent piece of feedback I got in this game was that walking up the staircase was very difficult in this game. This was related to a bugfix I put in mid-way through the jam - the staircase was too steep for the player to climb, so I made a tweak to how the player moves while grounded. Instead of applying lateral (XZ) motion along the plane defined by a normal of Y=Up, I apply lateral motion to the plane defined by the ground normal.

Grounded locomotion was updated so that the player's joystick movement was applied to the plane defined by the ground normal (right)

This caused issues because players were walking up the staircase successfully, but once they reached the top they had all of this upwards + forward velocity that wasn't getting resolved - the end result was them flying off of the staircase. This was something I missed in testing because I just got used to it. I think when fixing this for next time, I should try to kill the player's y velocity if they enter the in-air state, but there's still ground below them (ie: a short raycast check). This will require some playing around with, to avoid making platforming feel bad.

Enemies

The enemies were intentionally simple, since they were mainly a simple vehicle for the player having weapons in the first place. I didn't get too experimental here, mainly for a lack of time, what with my focus being on the physics grab system.

I think the crystal ball enemies were good foil for the sword character. It's really easy for the player to just run around with the sword held to the front/side, and so having an enemy that can tangle and block the player's sword was a good way to challenge the player. At some point I do want to introduce enemies that properly guard against the player in given directions, to switch up gameplay. Thinking something along the lines of Blade and Sorcery's enemies, which hold their weapons perpendicular to the player's held weapon in anticipation of the player's weapon.

The suit enemies are nothing special mechanically, I just thought the suit model looked cool! I did want a chance to try working with an animated humanoid enemy too, rather than relying on enemy models that came with animations built-in, like the ones in Wingmage VR (2025). There is a separate hitbox on their heads which will multiply incoming damage by 2x, so there is a little incentive to aim right. I think this would've been more interesting if there were less incentive to hit the suit enemies on the body - maybe the damage reduction on the suit body was dramatic, and the suits themselves were much larger, to really push usage of the gun against them.

Arena Management

I spent a good amount of time tooling to set up the enemy spawning, and so the arena was a dream to develop for. Huge shout out to ImGui, which made testing and debugging this part super easy. I had an ImGui window that mainly showed debug info about the active wave, but being able to start/stop/debug the wave was huge for me - it's something that I've missed coming from Unity and its custom inspectors.

Having all of the spawning info stored in a resource was really nice too - it was easy to tweak the intensity and tempo of the round at various points, just by dragging around elements of a list and playing with some timer durations. Being able to have one central place to say "I'm spawning enemy X, N times, at location Y" made design time really easy.

Environment

I'm really happy with the direction of the environment for this game. I'm still using asset packs that I purchased through humble bundle, and I'm really glad to actually be getting usage out of them lol

Terrain3D was something that I only briefly got to play with during the last jam, since I wasn't responsible for level design. It was really nice being able to work with it from start to end this time. Aside from a few snags here and there, it's a great system to use - very intuitive. I'm really happy that I figured out how to prevent enemies from falling through the floor in this one - that was a huge pain point in the last jam.

I think I'm starting to lock in an art style for my Godot games now. Working with paid assets is such a dream - I love the art style for my other games like Shattered Skies (2021) where I did all of the modeling and heavy-pixel texturing, but having a huge breadth of models to work with is huge. After disabling linear filtering on the textures for these models, things don't look too out of place with my pre-made low-res textures either. It's a whole thing having to start from 0 in a new engine - I miss my shaders, and my frameworks! I'm slowly working my repetoire up again though, and I'm happy with the results!

Summary

I think Crystal Bastion was a total success! The physics interaction system works, which was the main thing I wanted to try out, and I'm really glad that it feels as great as it does. Making an actual game around this system was a secondary objective, and so I paid more attention on my systems and less attention to the actual game design and playtesting as I should have (as usual), but it still turned out fun!

What went well

I'm really happy with how the physics interaction system turned out. The grab interactions feel very natural, and I only had to use a handful of janky bandaids to get it to a stable state. These are all kinks that I can work out when I go back to working on the grab interaction framework with more focus spent on a proper implementation, so no huge concerns there.

What didn't go well

I think as usual there's always more playtesting that could be done - it's always hard doing this in VR. Mainly in finding a community of folks with the hardware to play it, and the VR legs to withstand the physics vomit contraptions that I'm asking them to try out. That said, I'm always more interested in working on my systems and tools, rather than the end result - so I'm coming out of this happy as usual!

Next Steps

I am once again talking about wanting to work on Hexabody/HurricaneVR ports to Godot!

In terms of next steps, I have a lot of work I still want to put into my grab framework. I would love to clean this up and upload it to the asset hub in an experimental state - but I think I want to clean it up a little first. I also want to finish the 2-handed grab interaction functionality too, that's my main objective for personal development at the moment. The holstering system left a lot to be desired also. I think that refactoring this system to also be a grabber (like in Hurricane VR) makes sense to me, but that might be a little too much complexity that a holster system needs.

I do still want to work on my rigidbody player controller. At the moment I'm just pushing a capsule rigidbody around the scene, but there's a lot of functionality I don't support like climbing or swimming. I also want to implement physics hands like in Hurricane VR, so that there's a physics joint connecting the player's body rigidbody and the player's hands. This would let the player both push light objects around the scene with their hands, but also let the player move around the scene by pulling/pushing heavier/static environment.

Finally, I want to keep working on my shader implementations. I got a lot of mileage out of one shader that pans a voronoi texture - I'd love to have more versatile shaders like that ready to go for the next jam. It sounds like we just got access to reading/writing to/from the stencil buffer as part of Godot 4.5, so that could be fun to work with! Love a good outline on my models, but it's always been a pain point in VR.