[GRUEDORF] Kildorf's Devlog
-
@kildorf The map accordion system sounds like a good time-saver. I also really like the choice of over-the-shoulder battle camera angle and turn order toward the top of the screen.
-
Gruedorf Post 2022-06-15
- Skipped Gruedorf for 3 months
- Got and/or drew some real art
- Finished most of the core systems
- Lots of refactoring and bug fixes
- Built a demo of the first chunk of the game (sorta)
Hi again. In my last post I closed with the words “If it comes down to it, I’ll always choose putting time into the game over posting a devlog.” That turned out to be more prophetic than I anticipated! There’s a lot to talk about and I’m not sure how much I’ll actually manage to cover but here we go.
How’s Black Mountain?
It’s actually looking pretty good, if I do say so myself. I’ve been working on it close to every day since my last post, and it has changed a lot – looks like I’ve made 390 git commits since then. Obviously, nobody has the time for an exhaustive cataloguing of each thing I’ve done, so instead I’ll just describe where I am now.
At this point I have a demo, which seems to be about 30-60 minutes of playtime (depending on how quickly you zoom around, how much time you take to look at everything, etc etc). It’s essentially the opening of the game plus the game up to the first boss, without any of the optional side areas that will be littered around. I’ve had a few people play it, and it’s been a bit buggy but feedback has been essentially positive.
The Art
In the demo, there are no (well, almost) pieces of placeholder sketch art left; everything has been updated to some level of coloured and polished. Our cast is looking much better these days.
The world is also looking better.
A lot of the background painting there is the work of Hyptosis, which should be a familiar name around the Verge community. The small pieces that aren’t from him are from me, looking very carefully at what he painted and trying to match it. It sincerely made me a better painter, and I’m even actually starting to enjoy painting, which has never been my favourite.
The cast also has far less scribbly portraits now.
These are the handiwork of my friend Cil, who I am eternally indebted to now.
Campfires
I do want to actually talk about one of the new features I added, though! I’ve got a backlog of them to talk about, I suppose, so maybe I’ll manage to work through them over time.
Spread throughout the world, there are a number of campfires. You can kind of think of these like save points, though the game allows you to save anywhere you like. A checkpoint does autosave (along with ANY map change), and it also completely restore your health and energy, and remove all adrenaline (a system in combat that I’ve added), and respawning all monsters that have been defeated. It’s sort of a reset point to let you recover from the last section of the world and prepare for the next.
Growing up (if you’ll permit me to get a bit weirdly personal for a moment) I did a lot of camping and, especially as you get older and the world starts feeling a bit too big and a bit too small at the same time, there’s something liminal about sitting around a campfire with your friends. It’s easier to talk, it’s easier to reminisce and think. This is also true in Black Mountain – the characters are (through mostly but maybe not entirely their own faults) dealing with something much larger than they’ve had to deal with before, and only have each other to lean on. When they stop at a campfire, they might have something to say.
Technically speaking, the way this works is that I have a bank of campfire scenes (built with the same scene definition stuff I described in the last post; the campfire area is actually just its own map, internally), and at any given time I can throw one on a queue of campfire scenes. Every time you rest at a campfire, it checks to see if anything’s queued up, and if there is, it will play it. At the moment, this is mostly used just to provide another scene every few campfires that you encounter, to give a good idea how it works, but the idea is that if anything in particular happens, the next time you rest the characters might have something to say about it. I’ve thought about adding something the first time a character is knocked out in combat, for instance, or it could be used to add some reflection on the part of the characters after they’ve done something big.
The important part here is that it gives a little bit of space for the characters to talk about what’s going on, and give the player a bit more insight into what’s actually going on in their heads. I want to focus on making the characters relatable, and hopefully this will go a long way to doing that.
Back to Talking
I have been in a bit of a self-imposed hermit state for a while now, trying to grind out the main core features of the game (and also on helping with the demo for Alice is Dead, a remake of the classic Flash game, that’s in the Steam Next Fest! You should go check it out!). I’m in a spot now, though, where I need to return a bit to the world of the living, and so I’m going to try to start talking more about my game and what I’m up to, and that sort of thing! At a bare minimum, I’m going to try to get back on that Gruedorf horse and start posting here on a weekly basis again.
Cheers!
-
Devlog 2022-06-22
- Tweak movement code to make it a little less slippery and to have less “jittering” at corners.
- Add foot step sounds.
- Add a screenshot button.
- Actually implement status effects in battle.
- Implement a couple of localization-related things.
- A bunch of refactoring.
- Small bug fixes and various tweaks.
The merchant in Shoda’s Brook, aka “Uncle Reg”. Violet’s actual uncle, and just an overall good guy to Kiel and Omen.Another week of work! Got some new art from Hyptosis to put into the game to make the merchant’s area in town a little more cluttered.
Footsteps
I tweeted about this on Monday, and there’s a fun video to watch there, but to sum up, the player-controlled character now makes footstep sounds as they walk around. This is defined as a default for each map, plus I can define areas within the map that override that default. As an example, in the video in the tweet, the map’s default is the “grass” sound, with extra areas defined for dirt and wood.
I had actually done a little bit of investigation on this a while ago, but had decided to lay it aside for a while. It’s pretty straightforward, really, just play a sound effect every now and then as they’re walking, but my implementation has a few niceties in it that I want to talk about:
Timing: Godot has an AnimationPlayer built-in which lets you define animations on a timeline. One of the things you can add as a “keyframe” on the animation is a simple function call, so rather than trying to keep the steps synchronized correctly with timers or whatever, I just add a function call to play a footstep sound to the frames where the character’s forward foot strikes the ground. There are other ways you could accomplish this with an AnimationPlayer timeline, but I chose this so I have a little more control over which sound plays, exactly.
Override Areas: The areas for the sound effects are pretty straightforward StepEvents which push the new sound into a list when you enter the area, and remove the sound from the list when you exit it. The first pass of the implementation was a simple variable set onto the character’s footstep data, but this caused some problems when crossing from one area to another – it’s not stable whether the area-exit event or the area-enter event will fire first, even if you manage to get the edges of the two areas to line up exactly. If there’s any overlap it’s even harder to know if the area-exit handler should be touching the value or not. Having an array is a little more complex (and slightly less performant) but it is a lot easier to get right.
Sound Selection: The sound that actually plays gets selected based on whatever is at the front of the footstep list; if it’s empty, we use the default. Each type of footstep sound has a bank of 8-12 sounds to provide some variety, and the footstep code remembers the last one it played, and (so long as we have more than one footstep in the current category, because this isn’t my first time at the rodeo) it will ensure that we never play the same sound twice in a row.
Status Effects
An actual conversation I had on Monday with my friend E, who has been helping me a ton with testing the game:
Kildorf: I just implemented status effects in Black Mountain so the giant wasp monsters can poison you now
E: Goddamit
E: You put poison in your game?
E: Why don’t you just fill it with escort missions where the npcs you have to protect have 3 health tooStatus effects are a staple in JRPG combat systems. I had sketched out some of the system (and, truthfully, was already using exactly one status effect for the Defend action in combat) before, but I didn’t yet have the infrastructure for building attacks that set status effects. I started, of course, with the classic “poisoned” status, which damages you each turn. The wasp-looking monsters in the game (which are, by the way, named “Rumble Bees” because they are stinging insects that are ready to fight) use a Bee Sting attack, which have a flat 50% chance of poisoning you when they hit.
Refactoring
I had an idea of something that wasn’t Black Mountain that I wanted to prototype out quickly, just to see how fast I could. Since the idea used a top-down sort of RPG movement, I wanted to just reuse what I already have in Black Mountain. I think it’s pretty good! I started a new Godot project and started copying things over and found that, despite my best intents, a lot of it needed to be reorganized or deleted or whatever. Instead of doing a bunch of refactoring in some throwaway code, I decided I’d go do some refactoring in the Black Mountain codebase instead!
This is actually pretty important to me in the longterm as well, since (and this will be no surprise to anyone who has talked to me about game development for more than five minutes) I have a lot of games I want to make, including some direct sequels to Black Mountain, so it’s something I really should just take a bit of time to do every now and then anyway.
I never did actually get around to prototyping that other idea out.
Localization
Localization is something that is relatively straightforward to include from the beginning of your game, and incredibly difficult (or at least, tedious and annoying) to retrofit in. I’m trying to opt for the first approach, even though I don’t actually know whether Black Mountain will ever be localized. It’s just good practice anyway. So I took a bit of time to flesh out some of the stubs I had in place with TODO comments saying “when I want localization to work, do this”. It’s still not quite at the point that I could truly drop in a translation, but it’s definitely closer.
Controllers in Godot
Controllers are a bit of a nuisance.
They’re a great way to control a lot of games, especially if you have vague ambitions to port to a console. Godot does a lot for making it easy to use controllers, but there’s a sticking point that I encountered and I wanted to write up how I dealt with it. I’m not going to claim this is The Best Way or anything (there are certainly some… oddities to it) but Google told me that I’m not the first to encounter this problem, and there wasn’t any solution around that I felt was acceptable.
To sum up the backstory: A long time ago, a playing card company named Nintendo released a video game console known as the Famicom, later dressed up to look like the most boring piece of consumer electronics imaginable and released as the Nintendo Entertainment System for the North American market. Perhaps you’ve heard of it. It had a controller with an A button on the right and a B button to the left. Sega, their competitor, released a system, the SG-1000 on exactly the same day (July 15, 1983), but it only had one button on the controller so who cares – a couple years later, they released the Mark III with a two-button controller, labelled 1 and 2, with 1 on the left and 2 on the right. This is where it all went wrong.
To skip a few things historically-speaking, there are a number of common, popular layouts of controllers now (and none of them are made by Sega): the XBox layout with A on the bottom, the Nintendo layout with A on the right, and the Playstation layout with symbols instead of letters and inconsistent usage of those symbols internationally.
Images clipped from Gamestop’s store pagesGodot does a great job of supporting these different controllers, but it does so at a hardware/physical level. When you set up your input map, you are mapping an action to a physical button, whatever colour/label that button has. This is great if you’re relying on muscle memory that happens to match exactly what you think is right.
The input mapping wizard in Godot.Thing is, I’d like to, at some point, show buttons and the like that actually match (moreorless) what the controller actually looks like, and I would also like to match what the controller actually implies about what the correct confirm/cancel buttons should be. I also happen to have a Nintendo-layout controller (specifically an 8BitDo SF30 Pro) hooked up to my PC, so I get to see how many other games fail to do this at all. Anyway, seems pretty straightforward though: Godot lets you remap input via code, so just detect if the connected controller is a Nintendo layout controller and remap if necessary, right?
Unfortunately, there are only two pieces of information you get from Godot: the controller’s GUID and its name. This is great for displaying the name, potentially, or differentiating controllers manually… but I don’t really want to have to sit down and figure out the GUID for every possible Nintendo-layout controller that has ever been released (or will be released in the future?) So I decided to dig into where exactly Godot gets its gamepad information. Or, rather, dig BACK in to it, since I had reason to look this up in the past when I was testing someone else’s Godot game and discovered that my controller simply wouldn’t work at all with it.
Turns out, Godot gets its controller information from the SDL_GameControllerDB, a community-sourced database of controller GUIDs, names, and mappings. Godot has a copy of the main TXT file there in its own source, located at (at time of writing, anyway) https://github.com/godotengine/godot/tree/master/core/input. There’s also another file in there for Godot-specific mapping, mostly for browser compatibility, as near as I can tell? In any case, the mappings here are used internally to make the input system work, but does not appear to be exposed to game script at all.
So, I just grabbed my own copy of the TXT file and stuck it in my game source. It’s easy enough to parse, so at load time I now parse my own copy of the controller DB, and create a table in-memory of the mappings. I also, at launch, read the mapping for “all joysticks” out of the input map, store them in an object, and then remove the mappings. For every joystick that is currently connected (and again, any time a new joystick is connected), I re-add specific mapping for that joystick to the input map. Before I do that, though, I check the joystick’s GUID against my in-memory version of the SDL_GameControllerDB, and IF it has a map of
a:b1
(which I’m using as my signal that it’s a Nintendo layout) and apply any modifications needed. This also happens to clear up the fact that I had not correctly set up my input mapping before anyway so you could only ever use the first controller plugged in. All of this, of course, falls back to the default XBox layout if they GUID isn’t in the table, so it’s not going to blow up if someone plus in something unrecognized, and to support new controllers I should just have to update my copy of the .txt file.This also means that when I get around to putting in custom input mapping (which I consider a critical feature still to be done), I can use the same controller DB to determine what labelling should be displayed, and same anywhere in the game I want to display buttons in tutorials or whatever.
There’s still more to be done, of course – I’m not doing any remapping for Playstation controllers (since I don’t have one I can easily test with) and I know if I want to support Switch Joycons they’re a bit of a mess, so I’ll have to figure that out. Regardless, what I have in place should be pretty easily extensible for whatever controllers I need to give a little special care.
-
Devlog 2022-07-06
- made enemy “AI” a little better
- add a display of the enemy name during targeting
- tweak a few places to change how a selection is highlighted to make it more obvious
- some boring (to talk about) refactoring and trying things out that haven’t gotten anywhere yet
- started on building out the village of the fae
I missed last week for personal reasons, which also led to me not getting as much done since then as I’d normally like.
Enemies
Previously, enemies had only a half-implemented partial idea for deciding what action to use, exactly. Unless they had a special script for their combat (which I use exactly once), they would always use exactly one attack; the last one they had defined. Now, though, each monster has a table of attacks they can choose from, with a weight. Here are the actions for the Gremlin monster:
"actions": [ { "chance": 2, "skill": "m_slam", "skill_level": 1, "target": "random" }, { "chance": 1, "skill": "stone_spike", "skill_level": 1, "target": "random" } ],
I do a weighted selection from the table based on the “chance” field, and then look at target. At this point, I believe every monster has all its attacks target “random”, which just means a random target. I could have it target a particular character by name and, more usefully, I intend to give myself the ability to restrict targets based on things like “has Energy left”, “the most HP”, etc. Anyway, once a skill and target is chosen, I just apply the skill (at the given skill level; any skill can scale based skill level) to the chosen target.
Weighted Selections
Actually, I want to talk a bit more about this since it’s something I’ve implemented a bunch of times. I often find myself wanting to have a table of options with different likelihoods for each thing. What I do not want to do, though, is define a bunch of percentages that all add up exactly to 100% that I then have to manually tweak each time I add something to the list. What I definitely do not want to do is wrap my head around a random selection based on iterative “rolls”, even though that would be very easy to code – this would look like “start at A and flip a coin; if it’s heads choose A, but if it’s tails then flip another coin; if it’s heads, choose B, but if it’s tails then flip another coin…”.
So instead I do this weighted choice: I just give each thing in the table a “chance”, which can be an arbitrary integer. Two entries with the same chance have the same chance of being chosen, while an entry with twice the “chance” has… twice the chance of being chosen, compared to the other. It’s very easy to reason about, and the code isn’t that hard to write. It looks like this:
func weighted_rand_from(arr, weight_key="chance"): var chance_sum = 0 # start out by summing the "chance" values for the whole table for a in arr: chance_sum += a[weight_key] # notice that we use the [weight_key] parameter so it can be called whatever you need var n = randi_range(chance_sum) # generate a single random number for a in arr: # step through the array, n -= a[weight_key] # subtracting the chance of each entry from the "roll" if n < 0: # once we drop below 0, we've found the one we want! return a return arr[arr.size() - 1] # return the last thing in the array as a safe default
Yes, this does involved iterating over the whole array (potentially) twice, but unless you’re using very large arrays this shouldn’t be an issue. If it ever did, you’d be farther ahead to recalculate your table into something else for your export. I doubt I’ll end up needing to worry about this for Black Mountain.
Enemy Name Display and Highlighting
I mentioned the name of one of my monsters was named a Rumble Bee internally but had to just refer to it as “the wasp monster”, and I got a bit of feedback from a playtester that it’s nice to know what the monsters are called. I tend to agree, so I added a display for them when picking a target.
Twig Sprites are one of the first enemies you encounter and probably the easiest to defeat in the game.
Along with that you can see I’ve added an extra cursor to the list of monsters. I’ve added one to the list of party members as well.
Kiel is probably going to use First Aid on Omen or something, but who knows for sure.
I’ve also added a small tweak to the character selection in the menu, for viewing status, using items, etc etc. I bump their portrait up a bit just to make it a little more obvious who you have selected. This one was suggested by Hyptosis, and it’s small but I do think it helps with clarity.
The only way Violet will ever be taller than the other two.
Village of the Fae
The next area I’m working on filling out is the Village of the Fae.
The fairies are definitely friendly and definitely won’t feed your baby to a goat.
This is a very quick paint job (and not even finished) like the rest of my map painting. I just want it to be clear and playable for the time being – it will get much prettier in the future.
-
- packed up everything I owned except the stuff I sold or threw out
- moved, really far
- made a decision that I haven’t decided whether it’s good or bad yet
Some time has passed since my last post. I have a reasonably good excuse, I think – I moved me, my wife, my cat, and most of our stuff (what we could justify keeping anyway) thousands of miles across the country, and got a new job. Things have been a bit hectic (and, really, still are!)
Not Unproductive
That said, I haven’t gotten NOTHING done on Black Mountain. After driving across the country (and my stuff, separately, being shipped across) I discovered my desktop computer no longer boots. It doesn’t even really try to power on, so it’s probably the motherboard. I got a new laptop right away (since I didn’t have an alternative that was up to the task of what my new job was going to demand of it), but the files (other than what was committed to git) were locked away on the hard drive in the desktop. I got the new Godot 4 beta and started messing around with that (trying very hard not to think too hard about “what if all my original art files are gone”), and didn’t make anything particularly interesting but I got a feel for how stable the beta is (quite stable!) and what had changed (quite a bit!)
Eventually we got settled in a new apartment and I got myself an external enclosure for my hard drive. The hard drive was totally fine, all my files were there, and I decided that what I should do is just bite the bullet and migrate the game to Godot 4!
A Mistake… Maybe?
I have gone back and forth a lot on just how much of a mistake this was. I’m still in the middle of the process, though I’m making progress at least. Nearly every change from Godot 3 -> 4 seems to be good (the rest seem pretty neutral, honestly) but I have wrestled a lot with what the automatic upgrade process gave me.
To be clear: I went into this fully aware that I was doing something that wasn’t recommended, I have a backup of the original project so I can always go back to Godot 3 if I want, etc. I did the automatic migration with beta 2, and beta 3 is already out (and beta 4 should be out soon). The migration process is part of what they’re improving as they go, so in the future this should be a lot smoother
That said, it’s been rough. Thanks to Godot 3’s wide-spread usage of strings for things like deferred function calls and properties etc, it means that if they changed a name for something, the migration process has to update all instances of that name to the new one… even if that occurs inside of a string (and, apparently, inside of a comment). This meant things like having my “Fight On Touch” behaviour being renamed to “Fight Checked Behaviour”.
Another thing that ended up being surprisingly rough was the new asynchronous stuff – being able to use
await
was one of the changes I was most excited for, but it turns out I had made use of the old yield-based asynchronicity in ways that weren’t compatible with the new way of doing things! There were a number of places where I would hold onto a GDScriptFunctionState object from a call on a stack, for instance, and check each frame to see if it was completed. Turns out you can’t really do that anymore, though I have figured out how to rework things for the new system (at least, so far). I’ll talk more about that below.Overall, though, I do like Godot 4. I’ve said it’s rough, but I want to stress again that it’s the migration process that has been. Godot 4 itself is still a beta, it’s quite solid, and if I were starting a new project I would absolutely start it in Godot 4.
Many Async Calls at Once
In Godot 3, coroutines are done with
yield
, which is actually used to wait for a signal.def f(): print("A") yield(get_tree(), "idle_frame") print("B")
Calling
f()
here will print “A”, then wait for the “idle_frame” signal from the root tree (which is fired every frame), and then will print “B” – specificallyidle_frame
is triggered right before_process
is called one each of your objects.What yield actually does is pack up the function’s execution state into a
GDScriptFunctionState
object, and returns immediately, thus ending execution of the function. Whenever the signal you’re waiting on fires, it unpacks thatGDScriptFunctionState
object and resumes executing. Since it jumps right back into the execution, you can stick that yield wherever you need in the function and start it right back up there…def wait(time): var done = OS.get_ticks_msec() + (time * 1000.0) while OS.get_ticks_msec() < done: var t = OS.get_ticks_msec() yield(get_tree(), "idle_frame") return (OS.get_ticks_msec() - t) / 1000.0
This wait() function will keep
yield
ing out of the while loop every frame, and going right back in to check how much time has passed. Once it passes the requested threshold (time
in seconds) it will exit out of the loop and be done.The next important part is that async functions (aka coroutines) fire a signal called
completed
when they finish. (Technically this is probably not quite correct; I don’t thinkcompleted
is really a signal but it works like one, so I think it’s relatively safe to think of it that way.)Using that wait function above would look like…
def f(): print("A") yield(wait(5), "completed") print("B")
This version of
f()
would print A, wait 5 whole seconds (without locking up the engine, so rendering and input can still happen just fine) and then print B.This is an aside, but another cool thing with this is that you can get a value returned from an async call.
func wait_for_next_frame(): var t = OS.get_ticks_msec() yield(get_tree(), "idle_frame") return (OS.get_ticks_msec() - t) / 1000.0 func f(): print("A") var dt = yield(wait_for_next_frame(), "completed") print("B %s" % [dt])
Here,
wait_for_next_frame()
is a slightly fancier version ofyield(get_tree(), "idle_frame")
which will not only wait until the next frame, but will tell you how many seconds have passed since (which hopefully for you is a very small fraction of a second!)What happens, though, if you call a coroutine without yield?
func f(): print("A") var what = wait_for_next_frame() print("B %s" % [what])
In this case, you will print “A” and “B” immediately. Since there’s no
yield
, you’re not pausing execution. What you’ll see printed next to “B”, though, is aGDScriptFunctionState
object – in fact, it’s the one that represents the state of the coroutine when it calledyield
! That object has a function you can call to see if the function state is still valid; that is, can it still re-enter. You can hold onto that state object, and keep checkingis_valid()
, in fact, until it becomes false… which means the function has finally completed.So that means, say we have three coroutines we all want to run, but we don’t know how long they’ll all take. Say they’re part of a cutscene system – you want to have two characters walk around independently, and you also want to have a textbox show up that waits for the player’s input. You then want to end the cutscene once all three things have finished. You don’t know if the player will hit the button first or after the characters have finished moving, and you’re not sure exactly how long the characters will take to move (let’s say they’re doing some path finding or whatever).
Well… to cut to the chase, you write this function (pulled from Black Mountain’s Godot 3 source):
func multiyield(fs): yield(wait_for_next_frame(), "completed") var done = false while not done: done = true var s = "" for f in fs: if f is GDScriptFunctionState and f.is_valid(): done = false yield(wait_for_next_frame(), "completed") func f(): var async_things = [ move_first_character(), move_second_character(), show_textbox() ] yield(multiyield(async_things, "completed")
multiyield()
takes in a list ofGDScriptFunctionState
objects (at least, theoretically that’s what they are, but just in case we check to make sure). We start up a loop and in each iteration of the loop, we check every one of those objects. If any of them are still valid, we keep the loop going (but wait for the next frame to give things a chance to process). If every single one is no longer valid, we exit. In other words, we’ve created an async call that gloms all of those coroutines together and doesn’t complete until they’re all done!I will note that I am yielding for a single frame at the beginning because of a really cool quirk in the Godot 3 coroutines. Functions that don’t yield? They don’t have a “completed” signal. So if you have a normal, non-coroutine function, and you call
yield(foo(), "completed")
with it, it just blows up. If you look at my multiyield, you’ll notice that iffs
is empty, or if none of them are valid in the very first iteration, it’ll just fall through and return without needing to wait for a frame – meaning it would never yield at all. I throw in one frame of delay right at the beginning so I know that, no matter what, multiyield will always yield at least once. (This sucks; it’s a good thing I’m making a fairly slow JRPG and not a tight action game.)(At this point I should note that if you’re familiar with Promises in Javascript, this
multiyield()
is essentiallyPromise.all()
– I haven’t needed to write any of the rest of the various “multi-Promise” functions, but I’m reasonably sure you could write them all with a similar approach.)And Now in Godot 4…
So then what happens if you call this in Godot 4? Well, there is no
yield()
in Godot 4, so you’ll get that error. You might also see this error:Parser Error: Function "foo()" is a coroutine, so it must be called with "await".
Godot 4 is a lot more smart about what is and isn’t a coroutine, and it has introduced a new statement,
await foo()
which functions kind of likeyield(foo(), "completed")
but is a lot less kludgy. You no longeryield
in a way that returns a function state object – I am pretty sure Godot still uses those sameGDScriptFunctionState
objects internally but they don’t just hand them back to you when you call the coroutine in a particular way, which means you don’t shoot yourself in the foot by forgetting to yield on a coroutine’s completed “signal” so often! You canawait
on a normal non-coroutine function just fine, it simply continues on immediately! It’s great!It also means my exceedingly clever code to make a multiyield doesn’t work. Which was a bit disappointing. But I did find a work around, and it’s even kind of not-gross. Here’s what I’ve figured out:
func async_all(fs:Array): var id = 0 var rtnvalues = [] var completed = [] # use an array instead of an int because arrays are reference types rtnvalues.resize(fs.size()) var complete_one = func(rtnvalue, i): rtnvalues[i] = rtnvalue completed.push_back(i) for f in fs: var AO = AsyncWrapper.new(f[0], f.slice(1)) AO.async_done.connect(complete_one.bind(id)) AO.run() id += 1 while completed.size() < fs.size(): await get_tree().process_frame return rtnvalues class AsyncWrapper extends Object: signal async_done(returnValue) var _callable:Callable var _args:Array func _init(callable:Callable, args:Array): _callable = callable _args = args func run(): var r = await _callable.callv(_args) async_done.emit(r)
async_all()
here is my newmultiyield()
.AsyncWrapper
is a cool new Godot 4 internal class. You pass in a list of functions, just like with multiyield, but instead of calling those functions you pass a list of arrays likeasync_all([ [foo, "first_param", 2], [bar, "bars_param"] ])
which isn’t great but it works. (Note that callables in Godot 4 are first-class values, so no more passing string names around! I could probably replace these with .bind() calls instead, now that I think about it… hmm.) It sets up a few values, and an inline function
complete_one
(another new feature!), then sets about calling each one. It calls them with the AsyncWrapper class, whichawait
s the result of the call, and then triggers a signal – a signal we’ve connected to ourcomplete_one
function.complete_one
adds a value to thecompleted
array, and once thecompleted
array has grown in size to match the number of functions pass in… we’re done! As the comment notes, I’m using an array forcompleted
instead of just incrementing a number because (as near as I can tell) the anonymous inline function captures the value of variables at their declaration time, meaning every invocation ofcomplete_one
would think it was 0. Since arrays are reference values, every call is adding to the same array.Okay Time to Put the Keyboard Away
Okay I need to sleep and it is not making my explanations more coherent. Hopefully someone out there gleaned SOME useful information from this. Have a good week!
-
Devlog 2022-11-13
- continued Godot 4 migration
- got cutscenes playing again
- fixed game saving/loading
- fixed shadows
- discovered that _ready() works subtly differently now
- lots of fighting with UI
- various other fixes
You can actually get out of the house and talk to people again! The game crashes moments later when the shop interface attempts to load.Not a super exciting week to report on; just continued slogging through the update process. If nothing else, this process is helping me reacquaint myself with the code after not looking at it for a few months!
Just a few miscellaneous notes this week…
Godot 4 Changed _ready()
In Godot,
_ready()
is called when the Node is, well, ready to start its set up (after all its children in the scene tree have completed their_ready()
work). It’s where you do all your upfront initialization (usually), so it’s pretty important. In Godot 3, the_ready()
defined in every class in your inheritance hierarchy gets called, in order, with your “local”_ready()
being called last. This is different from how most function calls work, since they will call the “local” function and you have to explicitly call the super-class’s version by using.whatever()
.In Godot 4,
_ready()
is no longer special (and instead of just using a bare.
to call your super-class’s methods, you usesuper.
) so you have to explicitly callsuper._ready()
in your_ready()
method. I actually think this is good! Consistency is great! It was just a surprise since that’s not how it worked before, and I only figured it out after some trial and error.… And Some Other Things
There’s one other weird change that I’ve found, which I’m not as fond of. Classes in GDScript, especially in Godot 3, are a bit weird to work with as a data type themselves. It’s customary to override two particular functions,
get_class()
andis_class()
, so that you can query what something is. They’d look something likeextends Node class_name FooBar func get_class(): return "FooBar" func is_class(class_name): return class_name == get_class() or .is_class(class_name)
which lets you write code like
if thing.is_class(TheOneIWant):
, allowing for inheritance etc etc.Well, anyway, in Godot 4 you can’t do that because
get_class
appears to be special now and you can’t actually override it. I’m not sure if this is intentional or a current bug! Anyway, turns out that theis
keyword does what you need now anyway so there’s not really any real reason for doing all of this anymore anyway. You can just writeif thing is TheOneIWant
and it handles inheritance correctly.(I actually just popped open Godot 3.5 to see if I could figure out exactly how
is
works in Godot 3, but for some reason I couldn’t even get my test project to run and print anything at all to the console so if it’s worked this way all along, then I guess this more a note to myself than useful to anyone else.)UI Continues to Baffle Me
I come from a web development background, primarily in frontend development, so it’s not that UI in general is mysterious to me but it always takes me several tries to get Godot’s UI system to do what I want. I’m glad the UI system is there, it’s quite capable! I just always have to fight it. Some combination of me using it in some bone-headed ways and some interesting decisions on the part of the auto-migration has led me to having to fix a lot of my menus and such to get them looking right again. I’m using the process to try to understand the theme system a bit better and actually, you know, use that instead of having a bunch of manual overrides everywhere. So again, probably for the better! It’s just work.
The Show Goes On
I’m getting impatient to get back to actually pushing the game forward. I don’t know I’ll manage that by next Sunday, but progress is progress.
Have a great week!