<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Hyperlink Your Heart - godot4</title><link href="https://blog.hyperlinkyourheart.com/" rel="alternate"></link><link href="https://blog.hyperlinkyourheart.com/feeds/tag.godot4.atom.xml" rel="self"></link><id>https://blog.hyperlinkyourheart.com/</id><updated>2025-06-14T18:28:00+02:00</updated><subtitle>Until there's nothing left.</subtitle><entry><title>Lovely, Dark and Deep</title><link href="https://blog.hyperlinkyourheart.com/lovely.html" rel="alternate"></link><published>2025-06-14T18:28:00+02:00</published><updated>2025-06-14T18:28:00+02:00</updated><author><name>Kevin Houlihan</name></author><id>tag:blog.hyperlinkyourheart.com,2025-06-14:/lovely.html</id><summary type="html">&lt;p&gt;Post-mortem and results for my Ludum Dare 57&amp;nbsp;entry.&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a href="https://hyperlinkyourheart.itch.io/lovely-dark-and-deep" title="itch.io page for the game"&gt;&lt;img alt="Pixel art of a forest with a man on a bicycle in the foreground, and the text &amp;quot;Lovely, Dark and Deep&amp;quot;" src="https://blog.hyperlinkyourheart.com/images/lovely-dark-and-deep/cover_image.png" title="Lovely, Dark and Deep"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Back in April I took part in Ludum Dare 57, which had the theme &amp;#8220;Depths&amp;#8221;. As usual I was eager to take &lt;a href="https://github.com/khoulihan/digression" title="Digression repository on GitHub"&gt;Digression&lt;/a&gt; for a spin and see how much it had improved since its last outing for &lt;a href="https://hyperlinkyourheart.itch.io/out-of-gas" title="Out of Gas on itch.io"&gt;Ludum Dare 48&lt;/a&gt;, and this time I managed to come up with something that allowed me to do that. Coincidentally, the theme for &lt;span class="caps"&gt;LD48&lt;/span&gt; was almost the same (&amp;#8220;Deeper and Deeper&amp;#8221;), and I ended up making a somewhat similar game in some&amp;nbsp;respects!&lt;/p&gt;
&lt;h2&gt;The&amp;nbsp;Concept&lt;/h2&gt;
&lt;p&gt;The initial concept was one I&amp;#8217;ve had kicking around for a while, of a post-apocalyptic trading game with two phases to the gameplay - one where you travel between outposts by car, possibly having to engage in combat or other encounters, and another at outposts where you have to trade and maintain your car. I think of it as terrestrial &lt;a href="https://en.wikipedia.org/wiki/Escape_Velocity_(video_game)" title="Wikipedia entry for the game Escape Velocity"&gt;Escape Velocity&lt;/a&gt;, though the travel would undoubtedly be much simplified for a jam&amp;nbsp;game.&lt;/p&gt;
&lt;p&gt;Of course none of the elements of that idea are set in stone. I usually think of it as being set in a desert (the ol&amp;#8217; Mad Max influence I suppose), but in this case I wasn&amp;#8217;t sure if deserts &lt;em&gt;have&lt;/em&gt; depths? So that was out. I considered setting it in deep space instead (some sort of &lt;em&gt;non-terrestrial&lt;/em&gt; Escape Velocity, I suppose? What a notion), but that seemed too close to the &lt;a href="https://hyperlinkyourheart.itch.io/out-of-gas" title="Out of Gas on itch.io"&gt;game I made for Ludum Dare 48&lt;/a&gt; already. And so it became deep forest, and then the car became a bicycle because that seemed more&amp;nbsp;foresty.&lt;/p&gt;
&lt;p&gt;Most of the rest of the originally envisioned gameplay mechanics fell away as I tried to scope it to the 72 hours available. The travelling became just an opportunity for a bit of dialogue, the vehicle maintenance became a simple stat alongside others for the player, and the trading disappeared entirely. In an effort to introduce some gameplay beyond just engaging in dialogue, I added in a hand of randomly drawn cards at each location and a limited number of action points for their use. I think I had in mind the dice rolls in &lt;a href="https://citizensleeper.com/" title="Website of the Citizen Sleeper series of games"&gt;Citizen Sleeper&lt;/a&gt; when I came up with this, but I couldn&amp;#8217;t really remember how they worked in that&amp;nbsp;game.&lt;/p&gt;
&lt;p&gt;Considering it has been several months since the jam I don&amp;#8217;t think I could give you a blow by blow even if I wanted to, so I&amp;#8217;m just going to talk a bit about what worked and what&amp;nbsp;didn&amp;#8217;t.&lt;/p&gt;
&lt;h2&gt;What&amp;nbsp;Worked&lt;/h2&gt;
&lt;h3&gt;Digression&lt;/h3&gt;
&lt;p&gt;Digression worked really well for the most part. I think I encountered one bug, but I was able to work around it. I was able to churn out dialogue really quickly for the encounters and travel, and it was relatively easy to implement changes to the game state during dialogue as well (giving items and buffs and the like) - though that aspect of it could have been better, as I discuss&amp;nbsp;below.&lt;/p&gt;
&lt;h3&gt;Use of&amp;nbsp;Resources&lt;/h3&gt;
&lt;p&gt;I used &lt;code&gt;Resource&lt;/code&gt; types for all the items, buffs, curses, character classes etc. Although they were a bit of a mess and included some stuff that didn&amp;#8217;t end up getting used, it really paid off to work with resources right from the beginning. Often in the past I would implement something like the player&amp;#8217;s gun, for example, as just a scene or part of a scene, with the intention to rework it later to allow different guns defined by resources. And of course in a jam that never happens as time runs out and I get fatigued - so there ends up being only one gun, one pickup&amp;nbsp;etc.&lt;/p&gt;
&lt;p&gt;Probably the most obvious effect this had in this game is the character class selection. It doesn&amp;#8217;t add all that much, but I think it is a neat thing to have in a jam game and it gives the impression of&amp;nbsp;depth.&lt;/p&gt;
&lt;p&gt;So, big thumbs up to doing things the right way from the&amp;nbsp;beginning.&lt;/p&gt;
&lt;h3&gt;Audio&lt;/h3&gt;
&lt;p&gt;I used old reliable &lt;a href="https://lmms.io/" title="Website of the LMMS DAW"&gt;&lt;span class="caps"&gt;LMMS&lt;/span&gt;&lt;/a&gt; for the music this time around. Last time I tried some new tools and had a really bad time. I&amp;#8217;m really proud of what I made for this game, it had the exact spooky, mysterious quality I was going for. I don&amp;#8217;t think &lt;a href="https://www.beepbox.co" title="Beepbox - an online chiptune music tool"&gt;beepbox&lt;/a&gt; would have cut it this&amp;nbsp;time.&lt;/p&gt;
&lt;p&gt;The game didn&amp;#8217;t call for many sound effects, but the ones that are there, aside from the &lt;span class="caps"&gt;UI&lt;/span&gt;, are foley - always fun to do that and I think they work pretty&amp;nbsp;well.&lt;/p&gt;
&lt;h3&gt;Art&lt;/h3&gt;
&lt;p&gt;What can I say, I think I did a good job on the art. That&amp;#8217;s always been my strength anyway so big&amp;nbsp;woop.&lt;/p&gt;
&lt;p&gt;I am still using &lt;a href="https://github.com/Orama-Interactive/Pixelorama" title="Pixelorama on GitHub"&gt;Pixelorama&lt;/a&gt; and there are still a few things about it that I find grating, but I have gotten used to it enough that it doesn&amp;#8217;t get in my way. It was better suited to this game than my last one because most of the art was large background pieces rather than&amp;nbsp;tiles.&lt;/p&gt;
&lt;p&gt;Something that&amp;#8217;s kinda new is that I did lot of animation using tweens. Little things like the the back and forth motion of the bicycle and how it enters and exits the screen, and the distribution and flipping of the cards, were entirely done with tweens. They took only minutes to implement and I think they look great, and add so much to the aesthetic of the game. I only had to actually animate one thing by hand, the&amp;nbsp;pedalling.&lt;/p&gt;
&lt;h3&gt;The Humour,&amp;nbsp;apparently&lt;/h3&gt;
&lt;p&gt;That Sully, what a&amp;nbsp;card.&lt;/p&gt;
&lt;h2&gt;What&amp;nbsp;Didn&amp;#8217;t&lt;/h2&gt;
&lt;h3&gt;Digression&lt;/h3&gt;
&lt;p&gt;Though it certainly didn&amp;#8217;t let me down, I definitely noticed a few pain points in actually using it for a&amp;nbsp;game.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It was originally a main-screen editor, and at some point I decided it should be in the bottom dock - probably because Godot didn&amp;#8217;t provide an official way to add a main screen plugin and you had to sort of hack it in. However it requires a lot of space to use properly, and having it in the bottom dock was an impediment to that. I have moved it back to the main screen since the&amp;nbsp;jam.&lt;/li&gt;
&lt;li&gt;Having to manually manage the size of dialogue nodes was a hassle. I have since updated them to be resizeable width-wise, but to grow with the text length-wise. Much nicer to&amp;nbsp;use.&lt;/li&gt;
&lt;li&gt;Similarly, it was kind of a pain scrolling up and down a node when there was a long section of text. I have since partially implemented a mechanism to allow the dialogue node to be &amp;#8220;maximised&amp;#8221; when you want to focus on editing, and I will be doing the same for the choice&amp;nbsp;node.&lt;/li&gt;
&lt;li&gt;The lack of an open graphs list made it a bit awkward to move back and forth between graphs, as I had to find them in a scene or the filesystem every time. I have since added an open graphs list, as well as an anchors list for navigating around a&amp;nbsp;graph.&lt;/li&gt;
&lt;li&gt;The two different signals for dialogue and choice dialogue struck me as a little awkward in retrospect. I will be consolidating them I&amp;nbsp;think.&lt;/li&gt;
&lt;li&gt;There are a few problems with the variable management system. One is that they lacked defaults - I have added these since. Another is that the variables didn&amp;#8217;t mesh well with the overall game, i.e. game state and dialogue state felt like two separate things that I had to bridge. I&amp;#8217;m not sure how I am going to address that&amp;nbsp;yet.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Gameplay&lt;/h3&gt;
&lt;p&gt;The gameplay was pretty awful. I clearly do not really know how card games work. Limiting the player&amp;#8217;s actions to a random selection of cards just resulted in frustration, especially when it was not obvious that some actions required items, and when their use of the cards was also constrained by action&amp;nbsp;points.&lt;/p&gt;
&lt;p&gt;The stats generally degraded too slowly as well and there were unclear interdependencies between them - your health doesn&amp;#8217;t degrade until you exhaust your food or water stats&amp;nbsp;completely.&lt;/p&gt;
&lt;p&gt;The encounters were fun, I thought, but there were too few of them and they were too random - you would see the same ones over and over sometimes. Too many of them had disappointing outcomes, and if you didn&amp;#8217;t get the right cards there was nothing to&amp;nbsp;do.&lt;/p&gt;
&lt;p&gt;I think the random encounters worked a bit better in &lt;a href="https://hyperlinkyourheart.itch.io/out-of-gas" title="Out of Gas on itch.io"&gt;Out of Gas&lt;/a&gt; for two reasons: most encounters involved combat, so that is just inherently more exciting - and the fact that you were fleeing something, and could be killed, was slightly more compelling than a set of depleting&amp;nbsp;stats.&lt;/p&gt;
&lt;p&gt;Overall, the randomness just left the player with little sense of agency. I would have been better off dropping the gameplay altogether and just making it a short visual novel, with agency expressed only through dialogue choices. Though I&amp;#8217;m not sure I could write a compelling story for a visual novel in such a short timespan&amp;nbsp;either&amp;#8230;&lt;/p&gt;
&lt;h3&gt;Bugs&lt;/h3&gt;
&lt;p&gt;I introduced a bug at the last minute that prevented the game from ending when your health or vehicle state reached zero lol,&amp;nbsp;lmao.&lt;/p&gt;
&lt;h2&gt;Results&lt;/h2&gt;
&lt;p&gt;&lt;img alt="Results table - 257th overall, 600th in Fun, 256th in Innovation, 446th in Theme, 80th in Graphics, 49th in Audio, 48th in Humour, 70th in Mood" src="https://blog.hyperlinkyourheart.com/images/lovely-dark-and-deep/results.png" title="Not too bad. Could be better."&gt;&lt;/p&gt;
&lt;p&gt;Much better than my last two outings, and some good ratings in several categories, but definitely let down overall by the gameplay. I&amp;#8217;m a bit disappointed by the mood rating as I felt I nailed that, but it is a very subjective one of course - and it&amp;#8217;s still pretty&amp;nbsp;good!&lt;/p&gt;
&lt;p&gt;Some&amp;nbsp;notables:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;My highest ever rating, and placing, in humour (please&amp;nbsp;clap).&lt;/li&gt;
&lt;li&gt;Highest ever audio rating (by a hair), and&amp;nbsp;placing.&lt;/li&gt;
&lt;li&gt;First time being in the top 100 in four categories as a solo&amp;nbsp;dev.&lt;/li&gt;
&lt;li&gt;First time being in the top 50 in two categories. Third time being in the top 50 in any&amp;nbsp;category.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Second lowest&lt;/em&gt; rating in fun - only my Ludum Dare 38 entry, which was only the opening scene of an adventure game and had no gameplay, was&amp;nbsp;lower!&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt="Graph of audio placings" src="https://blog.hyperlinkyourheart.com/images/lovely-dark-and-deep/audioplacings.png"&gt;
&lt;img alt="Graph of humour ratings" src="https://blog.hyperlinkyourheart.com/images/lovely-dark-and-deep/humourratings.png"&gt;&lt;/p&gt;
&lt;h2&gt;Post-Jam&lt;/h2&gt;
&lt;p&gt;Nah.&lt;/p&gt;
&lt;h2&gt;Future&amp;nbsp;Developments&lt;/h2&gt;
&lt;p&gt;I was inspired to work on some sweeping improvements to Digression and have made a lot of progress, but there is also a lot of stuff broken and partially completed now. I will post about the changes in more depth when they are&amp;nbsp;done.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of dialogue and choice nodes in Digression" src="https://blog.hyperlinkyourheart.com/images/lovely-dark-and-deep/digressioncurrentstate.png" title="An example of some of the changes I've made"&gt;&lt;/p&gt;
&lt;p&gt;I also created a set of placeholder graphics to use in future jams. I used the default Godot robot sprite in this game, but it is not ideal in some ways because everything ends up looking the same, and when you put in the actual art you have to undo any scaling that was done to make the placeholder the correct&amp;nbsp;size.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of the game in early development, with the Godot logo everywhere" src="https://blog.hyperlinkyourheart.com/images/lovely-dark-and-deep/placeholders.png" title="Looks like Godot is already here"&gt;&lt;/p&gt;</content><category term="Game Development"></category><category term="ludum-dare"></category><category term="godot"></category><category term="godot4"></category><category term="pixelart"></category><category term="music"></category></entry><entry><title>From Hell’s Heart</title><link href="https://blog.hyperlinkyourheart.com/from-hells-heart.html" rel="alternate"></link><published>2024-05-14T22:46:00+02:00</published><updated>2024-05-14T22:46:00+02:00</updated><author><name>Kevin Houlihan</name></author><id>tag:blog.hyperlinkyourheart.com,2024-05-14:/from-hells-heart.html</id><summary type="html">&lt;p&gt;A post-mortem of my Ludum Dare 55 entry, From Hell&amp;#8217;s Heart, a twin-stick&amp;nbsp;shooter.&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img alt="Demon character from my game bound in a circle of protection" src="https://blog.hyperlinkyourheart.com/images/from-hells-heart/my_boy_scaled.png" title="What have they done to my boy??"&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://hyperlinkyourheart.itch.io/from-hells-heart" title="From Hell's Heart Itch Page"&gt;From Hell&amp;#8217;s Heart&lt;/a&gt; is my entry for Ludum Dare 55, the first Ludum Dare I have participated in since I made &lt;a href="https://hyperlinkyourheart.itch.io/out-of-gas" title="Out of Gas Itch Page"&gt;Out of Gas&lt;/a&gt; for Ludum Dare 48 in&amp;nbsp;2021.&lt;/p&gt;
&lt;p&gt;I was not thrilled with it overall (and neither was anybody else - but we&amp;#8217;ll get to that), but it was good to get into working on actual gameplay programming again after several years of only working on tooling, and it has inspired me to do a lot more gamedev work&amp;nbsp;since.&lt;/p&gt;
&lt;h2&gt;The&amp;nbsp;Concept&lt;/h2&gt;
&lt;p&gt;I was quite disappointed by the theme. I had an idea for the theme &amp;#8220;It Spreads&amp;#8221;, which was one of the final round themes, that I was very invested in. I was going to make a zombie shooter where the weapons were all spreadables like jam and nutella and so on, and I had a lot of fun art and gameplay ideas for&amp;nbsp;that.&lt;/p&gt;
&lt;p&gt;So when the theme of &amp;#8220;Summoning&amp;#8221; was announced I was disappointed. But, the theme is rarely the one I would prefer, so I got to brainstorming and came up with the idea of &amp;#8220;reverse-Doom&amp;#8221;, which evolved into something not explicitly Doom related, but where you play as a demon fighting soldiers nonetheless. For a brief moment I was even considering calling it &amp;#8220;What if Doom but you&amp;#8217;re the&amp;nbsp;Demon&amp;#8221;.&lt;/p&gt;
&lt;p&gt;The thing I liked about this concept was that it allowed me to address the theme in two ways - you are a demon who has been summoned, and you can summon other demons to help you. However, the fact that it is perfectly playable and beatable without ever summoning your demon friends diminishes this&amp;nbsp;somewhat!&lt;/p&gt;
&lt;p&gt;One of my hopes for this jam was to try out my dialogue graph editor, &lt;a href="https://github.com/khoulihan/digression" title="Digression GitHub Page"&gt;Digression&lt;/a&gt;, in another game, and unfortunately there was no place for it with this concept. I used it heavily in &lt;a href="https://hyperlinkyourheart.itch.io/out-of-gas" title="Out of Gas Itch Page"&gt;Out of Gas&lt;/a&gt; and &lt;a href="https://hyperlinkyourheart.itch.io/gophers" title="Gophers Itch Page"&gt;Gophers&lt;/a&gt; in the past, but it has been fleshed out significantly since then and is ready to be put through its paces. Oh&amp;nbsp;well.&lt;/p&gt;
&lt;h2&gt;Art&lt;/h2&gt;
&lt;p&gt;I couldn&amp;#8217;t get Pyxel Edit to run on my new computer when setting up for the jam (it is Windows only, and I have run it using Wine in the past), so I decided to branch out and try &lt;a href="https://github.com/Orama-Interactive/Pixelorama" title="Pixelorama GitHub Page"&gt;Pixelorama&lt;/a&gt; - a pixel art editor built in Godot. Although my muscle memory from Pyxel edit tripped me up constantly in minor ways it was a good experience overall and I think I will continue using it going forward, as I always prefer open-source and native applications when they are available. The only thing I really found lacking was the way it handles the grid/tiles. There are no tools for quickly copying and manipulating tiles, and the grid is specified in the global settings instead of being per-file, even though one grid is unlikely to suit different files even in the same&amp;nbsp;project.&lt;/p&gt;
&lt;p&gt;I also had to adapt the way I managed spritesheets, as Pyxel edit allows you to define multiple animations in a single file, with the frames being tiles, while Pixelorama will only animate the entire file as a single unit. As such, character sprites are spread across multiple files each. In Godot this means swapping the sprite texture for each animation, but that didn&amp;#8217;t seem to cause any&amp;nbsp;problems.&lt;/p&gt;
&lt;p&gt;I made some big mistakes with the level art early on, initially designing it at twice the appropriate resolution for the characters I had in mind. I was able to take the basic design ideas and downscale them into the final wall and floor tiles easily enough, but it still wasted a lot of time. I designed them for an auto-tiling approach which I developed a while ago, where a tool script populates the floor tiles when you update the&amp;nbsp;walls.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The initial oversized art I designed. The walls are about 5 times the height of the characters." src="https://blog.hyperlinkyourheart.com/images/from-hells-heart/humongowalls.png" title="Humongous walls, and some variants of the player character."&gt;&lt;/p&gt;
&lt;p&gt;I based the character shapes roughly on the design I came up with for &lt;a href="https://hyperlinkyourheart.itch.io/guerrilla-gardening" title="Guerrilla Gardening Itch Page"&gt;Guerrilla Gardening&lt;/a&gt; for Ludum Dare 41, which is very round and fun, and I&amp;#8217;ve found works really well for shooter games. It also features outlines for the characters which means there&amp;#8217;s less risk of ending up with characters that don&amp;#8217;t show up well against the&amp;nbsp;background!&lt;/p&gt;
&lt;p&gt;I was quite pleased with the art overall, though when I designed the wall and floor variants for the opening &amp;#8220;cutscene&amp;#8221; I realised that the game is far too red overall, as the characters looked much better against the bluer&amp;nbsp;background!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of part of the opening scene, with blue-grey crates against blue-grey walls and floor" src="https://blog.hyperlinkyourheart.com/images/from-hells-heart/crates.png" title="The crates had much less contrast though."&gt;&lt;/p&gt;
&lt;p&gt;I am determined next time I do a jam solo to not do full animations for each character or game element, and instead use the animation features of Godot to bounce, squash and spin things to bring them to life. This will give me much more time to create a variety of characters and objects, which I think would contribute more to a jam game than full run-cycle animations. The only place I used this kind of technique in this game was for the spinning and pulsing of the portal, and I think it was quite effective. People do some great things with this kind of animation and I am missing&amp;nbsp;out.&lt;/p&gt;
&lt;h2&gt;Music&lt;/h2&gt;
&lt;p&gt;Music is an element of jam games that I am always trying to find a new and better approach to, as it is something that I enjoy a lot but am not particularly skilled at. In the past I have used &lt;a href="https://www.beepbox.co" title="Beepbox"&gt;beepbox&lt;/a&gt;, &lt;a href="https://lmms.io/" title="LMMS Website"&gt;&lt;span class="caps"&gt;LMMS&lt;/span&gt;&lt;/a&gt; and &lt;a href="https://boscaceoil.net/" title="Bosca Ceoil Website"&gt;Bosca Ceoil&lt;/a&gt; with different degrees of&amp;nbsp;success.&lt;/p&gt;
&lt;p&gt;In the week prior to the jam I came across an application called &lt;a href="https://helio.fm/" title="Helio Website"&gt;Helio&lt;/a&gt; which I thought looked incredibly promising. It looked like it might be as easy to use as Bosca Ceoil but with a wider variety of possible instruments, so I was excited to try it&amp;nbsp;out.&lt;/p&gt;
&lt;p&gt;Another type of tool that I am always looking to bring into the fold is modular synth emulators. I have tried &lt;a href="https://vcvrack.com/" title="VCV Rack Website"&gt;&lt;span class="caps"&gt;VCV&lt;/span&gt; rack&lt;/a&gt; in the past but never managed to use it for a game, and I recently came across a fork of it called &lt;a href="https://cardinal.kx.studio/" title="Cardinal Website"&gt;Cardinal&lt;/a&gt; and had been practicing with that. I discovered that I could use Cardinal as a plugin for Helio and create instruments in it to control using Helio, so I planned to either do that or create a patch to generate all the music for the game, depending on what idea I was working on. The concept for From Hell&amp;#8217;s Heart didn&amp;#8217;t really seem to call for techno or ambient music, so I ended up trying the&amp;nbsp;latter.&lt;/p&gt;
&lt;p&gt;Unfortunately, things did not go smoothly at all with creating the music. I found Helio to be lacking in some essential features, and with a confusing menu system. It crashed constantly, forgetting instrument settings each time and sometimes changing the volume of notes or moving them around arbitrarily. I think the interface with Cardinal may have been responsible for some of this, and ultimately abandoned trying to use it for any instruments in favor of a selection of sound fonts. There was no way to preview the sound fonts in Helio, and I ended up previewing them on the command line using &lt;code&gt;fluidsynth&lt;/code&gt;, but not all of the ones I found that I liked worked when loaded into instruments in Helio. It was just a frustrating mess all&amp;nbsp;around.&lt;/p&gt;
&lt;p&gt;Most bafflingly in terms of missing features in Helio, I couldn&amp;#8217;t find a way of creating more than one pattern for an instrument, so I ended up having to use multiple tracks of the same instrument when I wanted a different&amp;nbsp;pattern.&lt;/p&gt;
&lt;p&gt;It also lacks a central mixer where effects can be applied. Instead you can add effects on a per-instrument basis. I tried to do this in Cardinal as well, but again abandoned that as time dragged on and the application kept&amp;nbsp;crashing.&lt;/p&gt;
&lt;p&gt;So overall it was a bit of a nightmare, took five precious hours, and the track ended up being overly repetitive, with just one melody repeated over and over with the only variation being different instruments coming in and out. I hated it when I was done, but it grew on me a bit when I put it in the game. It did kind of have the mood I was aiming&amp;nbsp;for.&lt;/p&gt;
&lt;p&gt;Every time I try to make music in Linux I feel like I am missing out on some brilliant workflow that ties a variety of different applications together with their different specialities, but I am just failing to grasp how it all works, and this time was no different. Back to &lt;span class="caps"&gt;LMMS&lt;/span&gt; next time I&amp;nbsp;think&amp;#8230;&lt;/p&gt;
&lt;h2&gt;Sound&amp;nbsp;Effects&lt;/h2&gt;
&lt;p&gt;Somewhere that I did get to use Cardinal was for the sound effects! I mostly used the Audible Instruments synth modules with a variety of different setting, and used &lt;a href="https://www.audacityteam.org/" title="Audacity Website"&gt;Audacity&lt;/a&gt; to record different notes being played for variety. Some of the results I was really pleased with (the demon snarls), others much less so (the enemy voices, which sounded like robots saying random words), but overall it worked pretty well. I would have preferred to do some foley stuff but there wasn&amp;#8217;t time, and it was a step up from &lt;a href="http://www.drpetter.se/project_sfxr.html" title="SFXR Website"&gt;&lt;span class="caps"&gt;SFXR&lt;/span&gt;&lt;/a&gt; at&amp;nbsp;least.&lt;/p&gt;
&lt;h2&gt;Code&lt;/h2&gt;
&lt;p&gt;Since my long-time project, &lt;a href="https://gamejolt.com/games/just-a-robot/185852" title="Just a Robot GameJolt Page"&gt;Just a Robot&lt;/a&gt;, is a shooter, I had a fair bit of base code to plunder for this jam. I don&amp;#8217;t think I did or learned anything particularly interesting this time around, but the base code did include a technique for showing a character silhouette when they are obscured by a wall which is interesting enough, and some automatic configuration of floor and mask tilemaps to allow rooms to be banged out quickly with little possibility of&amp;nbsp;error.&lt;/p&gt;
&lt;p&gt;The trick to the silhouettes is to create a mask of the parts of the walls that should obscure game entities using a &lt;code&gt;BackBufferCopy&lt;/code&gt; node, and then check that mask in a shader on any sprite that should be obscured. Objects in the game do not need to be children of the &lt;code&gt;TileMap&lt;/code&gt;, and in fact there is a different &lt;code&gt;TileMap&lt;/code&gt; for walls and&amp;nbsp;floors.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of Godot editor scene tree showing mask TileMap and BackBufferCopy" src="https://blog.hyperlinkyourheart.com/images/from-hells-heart/backbuffer.png" title="Yes I used nodes as &amp;quot;folders&amp;quot; here, bad very bad."&gt;&lt;/p&gt;
&lt;p&gt;The mask &lt;code&gt;TileMap&lt;/code&gt; is populated automatically in the editor using a script like&amp;nbsp;this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;tool&lt;/span&gt;
&lt;span class="k"&gt;extends&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TileMap&lt;/span&gt;

&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;copy_map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb nb-Type"&gt;NodePath&lt;/span&gt;
&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;copy_from_layer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb nb-Type"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;copy_to_layer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb nb-Type"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;refresh_frequency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb nb-Type"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;

&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;onready&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_copy_map_node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;get_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;copy_map&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_editor_hint&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;_tool_process&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_tool_process&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;copy_map&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb nb-Type"&gt;null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;refresh_frequency&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;_copy_map_node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;get_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;copy_map&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;current_ticks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_ticks_msec&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;current_ticks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;refresh_frequency&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;current_ticks&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;_copy_map&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_copy_map&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cells&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_copy_map_node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_used_cells&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;copy_from_layer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clear_layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;copy_to_layer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_copy_map_node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_cell_source_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;copy_from_layer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_cell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;copy_to_layer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;_copy_map_node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_cell_source_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;copy_from_layer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;_copy_map_node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_cell_atlas_coords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;copy_from_layer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;_copy_map_node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_cell_alternative_tile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;copy_from_layer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fix_invalid_tiles&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The mask tileset just has 100% red everywhere that should be obscured, so the shader just checks for red in the screen texture and displays a grey colour instead of the sprite&amp;#8217;s texture anywhere it finds it, leaving the alpha&amp;nbsp;intact:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;shader_type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;canvas_item&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;uniform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hide_when_occluded&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;uniform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sampler2D&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SCREEN_TEXTURE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hint_screen_texture&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;filter_linear_mipmap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fragment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;vec4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;textureLod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SCREEN_TEXTURE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SCREEN_UV&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hide_when_occluded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;COLOR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;COLOR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rgb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;vec3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m m-Double"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It turned out that people really hated the tight corridors and enemies being obscured though, so I probably won&amp;#8217;t be using this technique&amp;nbsp;again!&lt;/p&gt;
&lt;p&gt;A similar script to the one that populates the mask also populates a &lt;code&gt;TileMap&lt;/code&gt; with floor tiles based on the wall tiles. The floor tiles have a different size than the wall tiles however, so it is a bit longer and more involved. I think I would prefer to just put floor and wall tiles on different layers of the same &lt;code&gt;TileMap&lt;/code&gt; in the future, or as &lt;code&gt;TileMapLayers&lt;/code&gt; or whatever is being introduced in Godot&amp;nbsp;4.3&amp;#8230;&lt;/p&gt;
&lt;p&gt;Another neat thing I did in the editor was to have spawn points for portals and enemies draw a line to the thing they are associated with, or an obvious warning if they are not configured correctly. This helped me ensure that everything was set up properly when rushing through the room designs in the last few hours of the jam! I am trying to get into the habit of adding this sort of tooling to everything that might benefit from&amp;nbsp;it.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of Godot editor showing lines from spawn points to associated entities" src="https://blog.hyperlinkyourheart.com/images/from-hells-heart/spawnlines.png" title="Lines, lines, everywhere are lines"&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;tool&lt;/span&gt;
&lt;span class="k"&gt;extends&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Marker2D&lt;/span&gt;


&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GameRoomSpawnPoint&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_editor_hint&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;_tool_process&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_tool_process&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;queue_redraw&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_draw&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_editor_hint&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb nb-Type"&gt;null&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;_draw_to_destination&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;_draw_warning&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_draw_warning&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;draw_circle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb nb-Type"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;15.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb nb-Type"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_draw_to_destination&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;draw_circle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb nb-Type"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;15.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb nb-Type"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GREEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;draw_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb nb-Type"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;global_position&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb nb-Type"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GREEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I also took from the base code a node-based state machine for enemy &lt;span class="caps"&gt;AI&lt;/span&gt;. This turned out to be a bit confusing and inflexible, and when I tried to introduce some new behaviour on the final day I ended up making the game crash constantly, and had to roll back. I&amp;#8217;m still unclear on exactly what went wrong there - I was getting null references due to the summoned allies despawning, even with null checks before referencing them. It was probably really obvious but I was exhausted by then. In any case there were other problems with the state machine I developed and I have started investigating the Godot plugin &lt;a href="https://github.com/bitbrain/beehave" title="Beehave GitHub Page"&gt;Beehave&lt;/a&gt; as an alternative for the&amp;nbsp;future.&lt;/p&gt;
&lt;p&gt;Something that did not work very well was the enemy navigation and avoidance. I completely misunderstood how the avoidance system was supposed to be used, so it was actually not in play at all. In experiments I&amp;#8217;ve done since the jam I got it working to some extent, but it seems like it does not work very well anyway, with agents just grinding to a halt in many circumstances even after avoiding an obstacle! The navigation itself would have worked much better with one small settings change of &lt;code&gt;path_postprocessing&lt;/code&gt; in the &lt;code&gt;NavigationAgent2D&lt;/code&gt; to &amp;#8220;edgecentered&amp;#8221;, as I discovered later. As I had it for the jam, navigation agents are always getting stuck on wall&amp;nbsp;corners&amp;#8230;&lt;/p&gt;
&lt;h2&gt;Results&lt;/h2&gt;
&lt;p&gt;My game got lots of nice commments, and also a lot of complaints about out-of-bounds glitches, enemies being obscured by the walls, and being too&amp;nbsp;easy.&lt;/p&gt;
&lt;p&gt;I was uninspired by the theme this time around and my skills were somewhat rusty in every area, and the game was generic and buggy as a result, so I was unsurprised by the negative comments. I did think it looked good and was reasonably juicy and sounded alright. I wasn&amp;#8217;t expecting great overall ratings, but I did think it would do ok in the graphics category at least. However, the ratings were quite bad across the board, at least compared to previous&amp;nbsp;jams!&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: left;"&gt;Category&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Rating&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Placing&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Percentile&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Overall&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.571&lt;/td&gt;
&lt;td style="text-align: right;"&gt;574&lt;/td&gt;
&lt;td style="text-align: right;"&gt;65th&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Fun&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.63&lt;/td&gt;
&lt;td style="text-align: right;"&gt;418&lt;/td&gt;
&lt;td style="text-align: right;"&gt;75th&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Theme&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.413&lt;/td&gt;
&lt;td style="text-align: right;"&gt;792&lt;/td&gt;
&lt;td style="text-align: right;"&gt;52nd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Innovation&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.739&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1080&lt;/td&gt;
&lt;td style="text-align: right;"&gt;35th&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Humor&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.609&lt;/td&gt;
&lt;td style="text-align: right;"&gt;823&lt;/td&gt;
&lt;td style="text-align: right;"&gt;50th&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Graphics&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.935&lt;/td&gt;
&lt;td style="text-align: right;"&gt;428&lt;/td&gt;
&lt;td style="text-align: right;"&gt;74th&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Audio&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.619&lt;/td&gt;
&lt;td style="text-align: right;"&gt;343&lt;/td&gt;
&lt;td style="text-align: right;"&gt;79th&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;Mood&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.457&lt;/td&gt;
&lt;td style="text-align: right;"&gt;686&lt;/td&gt;
&lt;td style="text-align: right;"&gt;59th&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This game did worse in almost every category in percentile terms than my Ludum Dare 38 entry which I didn&amp;#8217;t even complete - it was just an opening scene with a short dialog and no gameplay. Quite a drop from my &amp;#8220;Gophers&amp;#8221;&amp;nbsp;peak!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Ratings Graph" src="https://blog.hyperlinkyourheart.com/images/from-hells-heart/ratingsgraph.png"&gt;
&lt;img alt="Percentile Graph" src="https://blog.hyperlinkyourheart.com/images/from-hells-heart/percentilegraph.png"&gt;&lt;/p&gt;
&lt;h2&gt;Post-jam and&amp;nbsp;Take-away&lt;/h2&gt;
&lt;p&gt;I won&amp;#8217;t make my usual promise to work on a post-jam version of this game, both because it&amp;#8217;s not interesting enough to be worth it, and because I never keep that promise&amp;nbsp;anyway!&lt;/p&gt;
&lt;p&gt;Instead I have been inspired to start working on &lt;a href="https://gamejolt.com/games/just-a-robot/185852" title="Just a Robot GameJolt Page"&gt;Just a Robot&lt;/a&gt; again since the jam, trying to get combat and enemy behaviour working the way I have always envisioned them, to see if it would actually be fun. The idea is to make it a cover-based shooter where the enemies abilities are close to the player&amp;#8217;s, more or less, and combat feels weighty and dangerous in a way that is distinct from bullet-hell&amp;nbsp;shooters.&lt;/p&gt;
&lt;p&gt;I successfully implemented a cover system for the player, but when I went to enable the enemies to use it and switch to using Beehave for their &lt;span class="caps"&gt;AI&lt;/span&gt; I quickly discovered that the way I had built them so far was too centralised and inheritance based, so after watching a few tutorials and considering things a bit I decided to start again mostly from scratch with a more composition-based approach. This is working really well for the player so far and I&amp;#8217;m nearly back at the same point as I was&amp;nbsp;previously.&lt;/p&gt;
&lt;p&gt;I also thought about and experimented with tiling approaches a bit, creating both a very small scale autotiling tileset with multiple terrains, and a mockup of a more organic looking tileset with a wide variety of designs, angled walls and floor sections and the like. I am tired of creating boxy, uninteresting levels. I was initially inspired in my gamedev journey by the art and design of Hyper Light Drifter, and I want to get back to achieving something of that look and sense of verticality - maybe not in jam games, but in Just a&amp;nbsp;Robot.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Complex tiles mockup" src="https://blog.hyperlinkyourheart.com/images/from-hells-heart/HLD-like Room Experiment.png" title="Too bright for this game, but moving in the right direction I think"&gt;&lt;/p&gt;
&lt;p&gt;I started on a greybox version of the above to actually use in designing the game, but it&amp;#8217;s not quite finished&amp;nbsp;yet.&lt;/p&gt;</content><category term="Game Development"></category><category term="godot"></category><category term="godot4"></category><category term="gdscript"></category><category term="ludum-dare"></category><category term="music"></category><category term="pixelart"></category></entry><entry><title>I Accidentally Visual Scripting</title><link href="https://blog.hyperlinkyourheart.com/accidental-visual-scripting.html" rel="alternate"></link><published>2023-11-30T22:31:00+01:00</published><updated>2023-11-30T22:31:00+01:00</updated><author><name>Kevin Houlihan</name></author><id>tag:blog.hyperlinkyourheart.com,2023-11-30:/accidental-visual-scripting.html</id><summary type="html">&lt;p&gt;I seem to be accidentally implementing a visual scripting system as part of my&amp;nbsp;tooling&amp;#8230;&lt;/p&gt;</summary><content type="html">&lt;p&gt;For some months now I&amp;#8217;ve been trying to make some progress on the game I&amp;#8217;ve been &amp;#8220;working on&amp;#8221; for probably the better part of a decade, &lt;a href="https://gamejolt.com/games/just-a-robot/185852" title="Just a Robot on GameJolt"&gt;Just a Robot&lt;/a&gt; (yep, 7 years ago was the first post there, and it was already in progress for a few years before that!). I haven&amp;#8217;t really been working on it for most of that time, though it has always been in the back of my&amp;nbsp;mind.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Variations on the character art over the years, including sprites and a portrait" src="https://blog.hyperlinkyourheart.com/images/accidental-visual-scripting/art_changes.png" title="All I actually seem to do is redesign the art..."&gt;&lt;/p&gt;
&lt;p&gt;While I have re-implemented some basic &lt;em&gt;gunplay&lt;/em&gt; mechanics, and experimented with auto-tiling and the like, my main task has been improving the editor for cutscene graphs that I designed previously (and which I&amp;#8217;ve &lt;a href="https://blog.hyperlinkyourheart.com/coroutine-callbacks.html" title="Coroutine Callbacks"&gt;mentioned&lt;/a&gt; a &lt;a href="https://blog.hyperlinkyourheart.com/all-my-yields.html" title="All My Yields post"&gt;few times before&lt;/a&gt;) based on my experience of using it in a &lt;a href="https://blog.hyperlinkyourheart.com/gophers-post-mortem.html" title="Gophers Post-mortem"&gt;couple&lt;/a&gt; of &lt;a href="https://blog.hyperlinkyourheart.com/out-of-gas-post-mortem.html" title="Out of Gas Post Mortem"&gt;jam games&lt;/a&gt;, and the anticipated requirements for this&amp;nbsp;game.&lt;/p&gt;
&lt;p&gt;I am also hoping that it will be something that other people might find useful, and that I can release in the Godot asset library. As such I try to do things in a generalised and user-friendly way, with nothing that would tie it specifically to my game, or rough edges that I would just ignore myself but be embarrassed if other people had to deal with&amp;nbsp;them.&lt;/p&gt;
&lt;p&gt;As a result, it&amp;#8217;s taking quite a&amp;nbsp;while!&lt;/p&gt;
&lt;p&gt;One thing I&amp;#8217;ve noticed is that as I try to maintain flexibility I seem to be implementing tiny haphazard visual scripting systems for calculating values and defining conditions. Of course the graph editing is itself a type of visual scripting, but that that was both what I was expecting to be designing, and has well established &lt;span class="caps"&gt;UI&lt;/span&gt; conventions in the engine. I don&amp;#8217;t think there are any conventions or controls for defining values and conditions in the way that I&amp;nbsp;am.&lt;/p&gt;
&lt;h2&gt;Variable&amp;nbsp;Changes&lt;/h2&gt;
&lt;p&gt;In the initial incarnation of the graph editor, it was possible to set the value of variables, and branch based on the value of variables, but the way it was implemented was&amp;#8230; not&amp;nbsp;good.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Variable names were entered as strings. This seemed like a problem to me - as the scope of a project grew I anticipated that it would become harder and harder to keep track of what variables were being used, and if they were entered correctly&amp;nbsp;everywhere.&lt;/li&gt;
&lt;li&gt;Values were also always strings - if you wanted a boolean, just enter &amp;#8220;true&amp;#8221; or&amp;nbsp;&amp;#8220;false&amp;#8221;!&lt;/li&gt;
&lt;li&gt;There was no scoping, just one global pool of variables for all&amp;nbsp;graphs.&lt;/li&gt;
&lt;li&gt;You could only assign constant values to variables. No incrementing or decrementing, arbitrary calculations, or using the values of other&amp;nbsp;variables.&lt;/li&gt;
&lt;li&gt;You could only compare to constant values for&amp;nbsp;branching.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="Screenshot of early version of the editor, showing string variable name and value fields" src="https://blog.hyperlinkyourheart.com/images/accidental-visual-scripting/dialogue_editor_example.png" title="This Naomi be Wolf"&gt;&lt;/p&gt;
&lt;p&gt;My initial changes to improve this situation were to introduce scoping and type definitions for variables. Variables could be scoped to the graph, to the area (i.e. &amp;#8220;level&amp;#8221; or &amp;#8220;room&amp;#8221; or however the game wanted to define it), or be global. They could also be &lt;code&gt;boolean&lt;/code&gt;, &lt;code&gt;int&lt;/code&gt;, &lt;code&gt;float&lt;/code&gt;, or &lt;code&gt;string&lt;/code&gt;. The &lt;span class="caps"&gt;UI&lt;/span&gt; would change to reflect the type, so you would get a checkbox for booleans and a numbox for&amp;nbsp;numbers.&lt;/p&gt;
&lt;p&gt;This was better, but still kind of awful, because everywhere a variable was used the scope and type would have to be selected again! Unlike in code there was no way to declare a variable once and then use it elsewhere with its type and scope already&amp;nbsp;known.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of one of the graphs from my game &amp;quot;Out of Gas&amp;quot;, showing scoped boolean variables with values set by checkboxes" src="https://blog.hyperlinkyourheart.com/images/accidental-visual-scripting/teenagers_dialogue.png" title="Teenagers up to no good as usual"&gt;&lt;/p&gt;
&lt;p&gt;So next&amp;#8230; I implemented variable declarations, more or less! These are defined in the project, and anywhere that a variable is required in a graph it can be selected from a searchable&amp;nbsp;dialog.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Dialog for defining a variable" src="https://blog.hyperlinkyourheart.com/images/accidental-visual-scripting/variable_definition.png" title="Defining a variable"&gt;
&lt;img alt="Variable set node with the variable selection control and a boolean value" src="https://blog.hyperlinkyourheart.com/images/accidental-visual-scripting/predefined_variable_set.png" title="Variable Set node"&gt;
&lt;img alt="Dialog for finding a variable" src="https://blog.hyperlinkyourheart.com/images/accidental-visual-scripting/variable_selection.png" title="Selecting a variable"&gt;&lt;/p&gt;
&lt;p&gt;Now I felt like I was getting places, though the problem of only being able to set and compare constants remains, and is what I&amp;#8217;m currently tackling. But more about that in a&amp;nbsp;minute.&lt;/p&gt;
&lt;h2&gt;Choice&amp;nbsp;Conditions&lt;/h2&gt;
&lt;p&gt;Another feature of the initial version of the graph editor was the ability to make dialogue choices only conditionally available to the player - for example making a choice dependent on having encountered a particular character or completed a particular quest. This was of course based on the comparison of a variable to a string constant, so it had all the same deficiencies as everything else about the variables, as well as a few of its&amp;nbsp;own:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Only one variable could be used for each&amp;nbsp;choice.&lt;/li&gt;
&lt;li&gt;The only comparison available was equality, no greater or less than or anything like&amp;nbsp;that.&lt;/li&gt;
&lt;li&gt;There was no possibility of&amp;nbsp;negation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The minimal improvement would be to allow a comparison of a single variable with a selectable operator, and a constant value. But what if the choice depends on the value of multiple variables? What if you want to make different choices available if the player has encountered a character but not completed a quest, than if they have done both? This could maybe be achieved using branching nodes to set a third variable, but that seemed like jumping through a lot of unnecessary hoops. I wanted to be able to define complex conditions directly on the&amp;nbsp;choices.&lt;/p&gt;
&lt;p&gt;My solution was to add a dialog where an arbitrarily complex condition can be defined - though currently only with constants on the right side of any&amp;nbsp;operator.&lt;/p&gt;
&lt;p&gt;&lt;img alt="A screenshot of the condition definition dialog alongside the node that it was invoked from" src="https://blog.hyperlinkyourheart.com/images/accidental-visual-scripting/farnsworth.png" title="The Farnsworth Condition"&gt;&lt;/p&gt;
&lt;p&gt;As you can see above, the condition is structured as a tree with boolean operators grouping the results of comparison operators. The whole condition is summarised as its (frankly much more understandable) equivalent&amp;nbsp;GDScript.&lt;/p&gt;
&lt;p&gt;So mission mostly accomplished - it was now possible to define conditions on choices with an arbitrary number of clauses. However, it was starting to seem like a lot of &lt;span class="caps"&gt;UI&lt;/span&gt; complexity to achieve something that is quite simple to do in code - almost like a type of visual&amp;nbsp;scripting&amp;#8230;&lt;/p&gt;
&lt;h2&gt;Visualising&amp;nbsp;Values&lt;/h2&gt;
&lt;p&gt;Now it&amp;#8217;s time to tackle the other side of the equation - the values to set or compare the chosen variables&amp;nbsp;to.&lt;/p&gt;
&lt;p&gt;For setting variables, I wanted it to be possible to increment or decrement values as well as setting static ones&amp;#8230; But why not also allow values to be multiplied or divided? What if you want to set a variable to 2x another variable, or the result of a more complex calculation? These don&amp;#8217;t seem like particularly likely requirements for my game, but why reduce the flexibility by only allowing a handful of fixed&amp;nbsp;operations?&lt;/p&gt;
&lt;p&gt;For comparisons, I initially only considered allowing a choice between a constant or another variable. But would these not also benefit from more flexibility? What if you want to check if a variable is greater than half another variable? If setting variables was as flexible as described above, this could be achieved by setting a temporary variable, but that seems unnecessarily round-about and annoying to have to do. Since the task is the same for both situations (obtain a value for the right side of an operator), it seemed like it would be prudent to create one control that would cover&amp;nbsp;both.&lt;/p&gt;
&lt;p&gt;Another factor is that most of the above concerns only apply to integer and float variables. Booleans have another set of operators that might be applied to them. Strings have a more restricted set of operators, but there are a variety of functions that you might want to apply, or methods that you might want to call on them - &lt;code&gt;to_lower&lt;/code&gt;, &lt;code&gt;rstrip&lt;/code&gt;, &lt;code&gt;replacen&lt;/code&gt;, etc. In fact, the same might be true of integers and&amp;nbsp;floats&amp;#8230;&lt;/p&gt;
&lt;p&gt;With all that in mind the requirements have become quite complex - I&amp;#8217;m faced with implementing a small but significant subset of GDScript in a &lt;span class="caps"&gt;GUI&lt;/span&gt;!&lt;/p&gt;
&lt;p&gt;The design I have so far allows for multiple variables or constants to have operators to be applied between them, grouped by brackets if necessary, and for a selection of appropriate (for the type) functions to be&amp;nbsp;called:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of the design of the proposed value calculation control in the designer" src="https://blog.hyperlinkyourheart.com/images/accidental-visual-scripting/value_calculation_mockup.png" title="That's a really long and weird way to write 1..."&gt;&lt;/p&gt;
&lt;p&gt;One thing that annoys me about this is that it&amp;#8217;s much the same structure as is involved in creating the conditions (a tree), but uses completely different controls and looks and works completely differently. It&amp;#8217;s going to look quite strange when they are side by side in the conditions dialog&amp;#8230; However, the tree control used for the conditions probably suits that better because the elements need to be selectable, and the &amp;#8220;prefix notation&amp;#8221; also suits it better I think, while it would be unfamiliar for most people for mathematical&amp;nbsp;operators.&lt;/p&gt;
&lt;p&gt;I can&amp;#8217;t really tell at this point if this is at all intuitive or if there&amp;#8217;s an obviously much simpler solution that I&amp;#8217;ve overlooked. But it certainly makes me long to be able to just enter the values as code, regardless of their&amp;nbsp;composition.&lt;/p&gt;
&lt;h2&gt;Could I Uuuh&amp;#8230; Do&amp;nbsp;That?&lt;/h2&gt;
&lt;p&gt;I have been thinking about what would be involved in allowing the user to enter conditions and values as the GDScript code they would likely already be familiar&amp;nbsp;with.&lt;/p&gt;
&lt;p&gt;Godot includes an &lt;code&gt;Expression&lt;/code&gt; class which can be used to parse and execute arbitrary expressions, so conceivably that could be used to run whatever the user input, and make the entire GDScript language available to them. It looks like it can even parse the text for errors before running it, which would be all that I would want to do in the editor anyway. One likely difficulty is that the referenced variables would not actually be GDScript variables at runtime, but entries in one of several dictionaries, so I might have to write my own parser anyway to pick those out and replace them. I&amp;#8217;m not sure I really want to do&amp;nbsp;that.&lt;/p&gt;
&lt;p&gt;If that didn&amp;#8217;t work out, the alternative would be to parse the input myself (even more parsing!) and convert it into the same resource structures that I&amp;#8217;m intending to have the &lt;span class="caps"&gt;UI&lt;/span&gt; create. This would likely be much more limited in which language constructs and operations it could support - and that might cause confusion or frustration for the&amp;nbsp;user.&lt;/p&gt;
&lt;p&gt;And, there are reasons why allowing these things to be defined as code might not be a good idea&amp;nbsp;anyway:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I don&amp;#8217;t really like the idea of having to switch from a &lt;span class="caps"&gt;GUI&lt;/span&gt; way of doing things to a code way of doing things when the rest of the plugin is very much &lt;span class="caps"&gt;GUI&lt;/span&gt;&amp;nbsp;driven.&lt;/li&gt;
&lt;li&gt;I don&amp;#8217;t like the fact that the predefined variables would not be selectable, negating some of their utility. Or at least, to make them selectable I would have to implement some sort of auto-completion on top of everything else, which might be beyond&amp;nbsp;me! &lt;/li&gt;
&lt;li&gt;Some people use C# with Godot, and it&amp;#8217;s unclear if the &lt;code&gt;Expression&lt;/code&gt; class can parse C# - I suspect it&amp;nbsp;can&amp;#8217;t.&lt;/li&gt;
&lt;li&gt;Using the &lt;code&gt;Expression&lt;/code&gt; class would likely allow arbitrary GDScript to be entered and executed during graph processing. That might be too much flexibility! If I want to allow that it will be its own node&amp;nbsp;type.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One other &lt;span class="caps"&gt;GUI&lt;/span&gt; option I can think of would be to allow value calculations and conditions to be defined using their own type of graphs. This might have the advantage of using well established &lt;span class="caps"&gt;UI&lt;/span&gt; conventions and existing controls. On the other hand, a graph editor is not ideal for defining a tree, and it would probably appear even more unnecessarily complex and confusing for the undoubtedly most common use cases of setting variables to constant values and defining simple conditions based on single variables. It would also not be any less work for&amp;nbsp;me!&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;&lt;span class="caps"&gt;UI&lt;/span&gt; is hard &lt;i class="fas fa-sad-cry body-icon" /&gt; &lt;/p&gt;</content><category term="Game Development"></category><category term="godot"></category><category term="godot4"></category><category term="gdscript"></category><category term="cutscenegrapheditor"></category></entry><entry><title>All My Yield()s, Gone!</title><link href="https://blog.hyperlinkyourheart.com/all-my-yields.html" rel="alternate"></link><published>2023-06-23T17:10:00+02:00</published><updated>2023-06-23T17:10:00+02:00</updated><author><name>Kevin Houlihan</name></author><id>tag:blog.hyperlinkyourheart.com,2023-06-23:/all-my-yields.html</id><summary type="html">&lt;p&gt;The removal of yield() in Godot 4 means a new approach is required for some of its use&amp;nbsp;cases.&lt;/p&gt;</summary><content type="html">&lt;p&gt;In a &lt;a href="https://blog.hyperlinkyourheart.com/coroutine-callbacks.html" title="Coroutine Callbacks post"&gt;previous post&lt;/a&gt; I described a way to use the state object returned by a &lt;code&gt;yield()&lt;/code&gt; call to control the traversal of a graph - specifically, a graph describing a cutscene or dialogue - where some nodes in the graph require waiting on input from the user or some other event before&amp;nbsp;proceeding.&lt;/p&gt;
&lt;p&gt;In &lt;a href="https://godotengine.org/" title="The game engine you waited for."&gt;Godot 4&lt;/a&gt; the &lt;code&gt;yield&lt;/code&gt; function was replaced with the &lt;code&gt;await&lt;/code&gt; keyword. This has the same basic purpose: to suspend execution of the current function and return to the caller, to be resumed at a later time. However, it does not return the state object that &lt;code&gt;yield&lt;/code&gt; did, so there is no built-in way to resume the function from the caller (that I can see,&amp;nbsp;anyway).&lt;/p&gt;
&lt;p&gt;Fortunately, it is not difficult to recreate the functionality. The first thing we need to do is define a very simple class that we can include instances of in the signals from the graph&amp;nbsp;controller:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ProceedSignal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;signal&lt;/span&gt; &lt;span class="n"&gt;ready_to_proceed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;func&lt;/span&gt; &lt;span class="n"&gt;proceed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;ready_to_proceed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This class includes a signal and a method that the consumer of the graph can call to tell the graph controller that it can proceed to the next node - similar to the &lt;code&gt;resume()&lt;/code&gt; method on the coroutine state object in Godot&amp;nbsp;3.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;_await_response&lt;/code&gt; function which previously yielded to create a resumable coroutine state, now just returns a new instance of this class. It could alternatively just be created directly where this function is&amp;nbsp;called:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;func&lt;/span&gt; &lt;span class="n"&gt;_await_response&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ProceedSignal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In &lt;code&gt;process_cutscene()&lt;/code&gt; we now &lt;code&gt;await&lt;/code&gt; calls to process node types that require waiting on the consumer, rather than &lt;code&gt;yield&lt;/code&gt;ing&amp;nbsp;them:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;func&lt;/span&gt; &lt;span class="n"&gt;process_cutscene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cutscene&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;_graph_stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;_local_store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;_current_graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cutscene&lt;/span&gt;
    &lt;span class="n"&gt;_current_node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_current_graph&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;root_node&lt;/span&gt;

    &lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;_current_node&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;null&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_current_node&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;DialogueTextNode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_process_dialogue_node&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;_current_node&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;BranchNode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;_process_branch_node&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And when processing such a node, we just create the &lt;code&gt;ProceedSignal&lt;/code&gt; object, emit the relevant signal with it, and then await the &lt;code&gt;ready_to_proceed&lt;/code&gt; signal from&amp;nbsp;it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;func&lt;/span&gt; &lt;span class="n"&gt;_process_dialogue_node&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;

    &lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_current_node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;

    &lt;span class="n"&gt;var&lt;/span&gt; &lt;span class="n"&gt;character_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;null&lt;/span&gt;
    &lt;span class="n"&gt;var&lt;/span&gt; &lt;span class="n"&gt;variant_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;null&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_current_node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;null&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;character_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_current_node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character_name&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_current_node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character_variant&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;null&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;variant_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_current_node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character_variant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;variant_name&lt;/span&gt;

    &lt;span class="n"&gt;var&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_await_response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;call_deferred&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;_emit_dialogue_signal&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;character_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;variant_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;process&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ready_to_proceed&lt;/span&gt;

    &lt;span class="n"&gt;_current_node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_get_node_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_current_node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Nothing much changes from the consumer&amp;#8217;s point of view, it just needs to store the object and then call the &lt;code&gt;proceed()&lt;/code&gt; method when it&amp;#8217;s&amp;nbsp;ready:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;func&lt;/span&gt; &lt;span class="n"&gt;_on_cutscene_controller_dialogue_display_requested&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;character_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;character_variant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;process&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Hang on to the process object so we can tell the cutscene controller&lt;/span&gt;
    &lt;span class="c1"&gt;# to continue when we&amp;#39;re ready to proceed&lt;/span&gt;
    &lt;span class="n"&gt;_current_process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;

    &lt;span class="o"&gt;...&lt;/span&gt;


&lt;span class="n"&gt;func&lt;/span&gt; &lt;span class="n"&gt;_on_dialogue_display_continue_clicked&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;DialogueDisplay&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hide&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;_current_process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;proceed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That&amp;#8217;s all the changes required for this project! Of course, the coroutine state object also had a property indicating if it was resumable or not, &lt;code&gt;is_valid&lt;/code&gt;. It would not be difficult at all to reproduce this by simply adding such a property (perhaps behind a setter that would make it read-only except internally), and setting it to false once &lt;code&gt;proceed()&lt;/code&gt; is&amp;nbsp;called.&lt;/p&gt;
&lt;p&gt;It could also be expanded to allow more complex communication between the coroutine and the consumer, or to make a long running coroutine cancellable. The controller can only &lt;code&gt;await&lt;/code&gt; one type of signal to continue, but you could have it pass different instructions when resuming e.g. you could give it &lt;code&gt;stop()&lt;/code&gt; and &lt;code&gt;proceed()&lt;/code&gt; methods. Additional properties on the signal object could be used to pass back other data without having to pass it in the signal at&amp;nbsp;all.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;ProceedSignalType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;STOP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;PROCEED&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ProceedSignal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;signal&lt;/span&gt; &lt;span class="n"&gt;ready_to_proceed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;var&lt;/span&gt; &lt;span class="n"&gt;consumer_state&lt;/span&gt;

    &lt;span class="n"&gt;func&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;ready_to_proceed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProceedSignalType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;STOP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;func&lt;/span&gt; &lt;span class="n"&gt;proceed&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;ready_to_proceed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProceedSignalType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PROCEED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Cutscene Graph Editor&amp;nbsp;Status&lt;/h2&gt;
&lt;p&gt;The cutscene graph editor has been upgraded to support Godot 4, at a &lt;a href="https://github.com/khoulihan/godot4-cutscene-graph-editor" title="Cutscene Graph Editor project on GitHub"&gt;new home&lt;/a&gt;. I&amp;#8217;ve also added a bunch of minor features, such as multi-node deletion, copy &lt;span class="amp"&gt;&amp;amp;&lt;/span&gt; paste and duplication support, and support for dragging from an output port to create a new&amp;nbsp;node.&lt;/p&gt;
&lt;p&gt;I&amp;#8217;m now working on improving some parts of the tool that were lacking in flexibility. My current task is improving the addition of conditions to the choice and random nodes, which previously only allowed a single variable to be compared for equality to determine if a branch should be considered. The new system moves the condition specification &lt;span class="caps"&gt;UI&lt;/span&gt; out of the nodes themselves and into a dialog box, and allows any number of variables to be evaluated using a variety of different&amp;nbsp;operators.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The Farnsworth Condition" src="https://blog.hyperlinkyourheart.com/images/all-my-yields/graph.png" title="The Farnsworth Condition"&gt;&lt;/p&gt;
&lt;p&gt;Future plans include new ways of defining and interacting with sub-graphs, more flexible ways of manipulating variables, built-in variables and meta-data, and better ways of defining&amp;nbsp;characters.&lt;/p&gt;
&lt;h2&gt;Update: Pure&amp;nbsp;Signals&lt;/h2&gt;
&lt;p&gt;It occurred to me after posting this that there is an even easier way to achieve this - as long as you don&amp;#8217;t need to keep any state in the signalling object. Because Godot 4 allows you to pass signals and callables, you could just pass the signal to proceed with the signal that initiates the action. When the consumer is ready to proceed they can then just call &lt;code&gt;emit&lt;/code&gt; on&amp;nbsp;it.&lt;/p&gt;
&lt;p&gt;I might still prefer the other way of doing it because &lt;code&gt;_current_process.proceed()&lt;/code&gt; reads a little better than &lt;code&gt;_current_process_proceed_signal.emit()&lt;/code&gt;.&lt;/p&gt;</content><category term="Game Development"></category><category term="godot"></category><category term="gdscript"></category><category term="godot4"></category></entry></feed>