Postmortem: Optimising my XR Game Jam entry


After nearly two decades away from game jams, I recently participated in an XR Game Jam with my entry, "Flower Dome 3001". Here's a look back at the journey:

The jam was a great learning experience for me. It was my first time participating in a jam in almost 20  years, since I learned Game Maker as a teenager. Here's a post-mortem, as well as the optimisation measures to 

Initial Concept and Pivot

  • My initial concept was to build a gardening simulation game with procedurally generated plants, where you can mix genetic traits from seeds to discover new plants. 
  • I spent a day researching L-systems and implementing a basic script that could generate a mesh based on L-system parameters in real time. However, the framerate was not great even in this limited test scenario, and I was not up to the task of optimising this for an enjoyable VR experience as such a novice to Godot and especially XR development.
  • As such, I quickly pivoted to using some freely available low-poly flower assets - Free (CC0) Stylized Low Poly Flowers pack to use in your projects : r/gamedev (reddit.com)

Blender Animation Learning Curve

  • Next, I spent a whole day learning how to rig up armatures and create bloom animations for each of these flowers. This was a tedious process at first, as I was attempting to speed-run learning the animation interface, leveraged by my previous (limited, and dated) experience with Blender. Each time I animated a flower, I learned something new and by the time I had done a few, I decided to start the rest from scratch with these new techniques. I still didn't quite nail the weight painting, but got something "good enough" by just going overboard with the number of bones. (I say "overboard", but given the title of this devlog I should mention - I don't think this had any effect on the performance of my game, however it might if I implement physics/IK on these skeletons).
  • During this time, I also built models for the dome roof, dome floor, water fountain, and the tiered garden shelves.
  • For the first test flower, I also implemented a "fully grown" animation using periodic sine functions and tweening between the last few frames of the bloom animation. This looked great, however when it came to re-importing the flowers with all of the completed animations, I never got around to re-implementing this for the general flower class. The bloom animations look great, but then the flowers just sit there completely still, occasionally darkening to indicate they need more water.  I regret not dedicating some time to these animations before submitting to the jam, as they make the world feel so much more alive. 
  • I also had planned to utilise these skeleton rigs to make the flowers react to player touch using physics or IK, and this would have been a great opportunity to implement some haptics. Alas, I ran out of time for this.


Technical Challenges and Close Calls

  • In the hours before the initial deadline (before the 1 day extension), I tried implementing a menu screen - I envisioned the player camera floating above the desert, some distance away from the dome, and thought this should be pretty simple to implement. I proceeded to have a very hard time getting my character to float (somehow during this process, it never occurred to me to just place a large invisible collision platform below the player...), then entirely corrupted my project by adding a line of GDscript that attempted to disable the physics process for my PlayerBody on _ready(). Running the debug for this project would freeze my PC requiring a hard restart, then even after I removed that script and any reference to it from my project, it would refuse to start again. Even when I rolled back with Git, and after several restarts, attempting to debug would freeze my computer. Luckily, I had created a .zip backup a few hours before all of this, and was able to return to a smoothly running project without losing much work (I was able to retrieve my GDscripts from the more recent, broken project and just had to redo a few things in the editor). However, I was a little scarred after my previous experience out in the desert and didn't make another attempt to implement a menu before submitting to the game jam.
  • The next day, I experinced a similar PC-crashing error when I attempted to add a small omni light to each butterfly to simulate glowing eyes. This caused great instability and it took me another hour to get back on track again.

Scope and Time Management

  • So, given the above notes it should come as no surprise that the last few days of the jam were quite rushed, with my project going over-scope and coming out unfinished. When we received an "extra day" for the jam, I mostly spent that fixing bugs with the watering/planting system (central to the game), but then also spent a few hours adding the boid-style butterflies (let's call them boiderflies) and rushing to add a pickable rake that confusingly does nothing. I think the time spent on these last two points would have been better dedicated to any of the following:
    • Adding in-game instruction, with milestones and goalposts for the player to aspire towards and feel a sense of progression
    • Better balancing the watering/drying/growing mechanics
    • Adding haptics (one of the core rubrics for the jam ratings, which I neglected entirely for my entry)
    • Adding a menu, player preferences, and potentially the ability to save (all would have been easily available had I used the Godot XR template from the start - lesson learned)
    • Fixing the meta build and properly testing (I was skipping the crucial step where you enable the Meta plugin)
    • Most importantly... optimising!


Optimising my game post-jam

Yes, unfortunately the game I released for the jam is an unfinished and un-optimised mess. On my mid-range modern PC, I was achieving 30-40 fps and figured it felt "smooth enough". When I finally ran on the Quest 3, it was unplayable, probably 5-10 fps. After spending some time inspecting the profiler and my horribly rushed code, I was able to at least double this FPS and hit the refresh cap!

  •  When the player waters a pot with the watering can, the soil in the pot gradually becomes darker and eventually fills up with water. For my initial tests, I scripted each pot to check its water level, and update the soil and water meshes accordingly... Surprise surprise - duplicating, modifying and overriding the material for dozens of meshes each frame can take its toll on performance. Unfortunately I didn't notice the performance hit at the time, and though I had intended to write some smarter code, I forgot and never got back around to it. Of course, the fix was actually super simple:


  1. Define a variable for our soil mesh and set it at _ready
    @onready var dirt_mesh = %DirtMesh
    var soil_material: StandardMaterial3D
    func _ready():
         soil_material = dirt_mesh.mesh.material.duplicate()

  2. Use an intermediary "new_water_level" variable each frame in _process():    
    func _process(delta):
         var new_water_level = water_level - dehyde_rate * delta
         if watering > 0:
              new_water_level += water_rate * delta
              watering -= 1
         new_water_level = clamp(new_water_level, 0, max_water)
  3. (I also forgot to include the crucial delta here in the version I shipped out for the game jam... so watering  takes forever with a low frame rate... my bad...).


  4. Only update the dirt and water mesh when the value actually changes:  


    if water_level != new_water_level:
             water_level = new_water_level 
            soil_material.albedo_color =  get_dirt_color(water_level, max_water)
             dirt_mesh.set_surface_override_material(0, soil_material)
             scale_water_mesh(water_level, max_water)
             water_spray.emitting = (water_level == max_water)



  • Some incremental framerate improvements were achieved by removing alpha transparency from the water shader and water particle effects. This actually improved the look of the water fountain by hiding the water streams that clip into the pool at the bottom. I imagine the performance gains here were more substantial for the GPU-lacking Quest native build.


Lessons Learned

  • Solid foundation: Using the Godot XR template from the beginning would have provided essential features like menus and save functionality.
  • Prioritisation: Focus on central gameplay elements before adding peripheral features.
  • Continuous testing on target hardware: Regular testing on the Quest 3 would have revealed performance issues earlier.
  • Player guidance: In-game instructions and progression milestones are vital for player engagement.
  • Jam rubrics: I overlooked haptics, a key rating criterion for the jam.
  • Optimisation: Especially in XR, performance should be a constant consideration throughout development.

Despite the challenges and unfinished state of "Flower Dome 3001", this game jam was an invaluable learning experience. It has reignited my passion for game development and provided an excellent crash course in XR development. Moving forward, I'm excited to apply these lessons to future projects and continue exploring the possibilities of XR gaming.

Get Flower Dome 3001

Download NowName your own price

Comments

Log in with itch.io to leave a comment.

It's amazing how much you've learnt and accomplished during this one jam :)
I'm very inspired by your determination to improve all the weaknesses of the game, even after the submission.  Very excited to see how your new skills will be reflected in your future projects! ps. I love the pickable rake!