Getting the hang of procedural animation and Godot
This is probably going to be a big update, since it's my first and I only decided to start doing devlogs after a few months of progress.
Nightwatch started in Unity, like a lot of indie games. But honestly, although I like Unity and I've made games with it before, the documentation and general developer experience appears to be devolving over time. Jumping back in after a few years, I felt completely lost choosing between pipelines and trying to understand why shader graphs weren't working in 2D. Things just seem... disorganized for them right now.
So I decided to finally give Godot a shot, especially since I'm leaning toward 2D for this project.
Godot is very opinionated, which is nice. It even has a code editor built-in (although VS Code is still my preference). That means no fiddling with external tooling to try to get things like debug breakpoints - a huge win. I like not having to fight with tools when I'm also learning other, more important things.
With my assets all moved in, I decided to focus first on one of the more interesting problems for this project: zero-G movement. In my (still developing) vision for Nightwatch, the protagonist moves from low-G to zero-G spaces within her spacecraft fluidly, and part of the gameplay revolves around learning how to effectively move around the environment.
In order to do that right, I need to figure out how to animate her interactions with her environment effectively. I started with standard keyframe-based pre-defined animations, but it quickly becomes evident that in order to do things like reach out to grab the nearest wall, she's going to have to be far more reactive to her surroundings.
I started by watching this great talk by David Rosen, which was enlightening. Since I'm pretty comfortable in code already, just getting a grasp of the high-level concepts really helped set me on the path. Particularly, I was impressed with the "imaginary wheel" concept to control the speed and progress of a looping run animation. This stuff is really clever!
Now to write the code - luckily Godot's native GDScript is pretty straightforward, even if my Python is rusty.
I began by dropping all of my basic animation needs into a single node just to play around. To begin I made a basic rigged skeleton for my character sprite and loaded all the bones as
onready variables which I would need to animate.
I usually gravitate toward the hard problems first, so I ran straight to wall-clinging. It's definitely an interesting problem to solve - how do I make the character's arms reach out for a wall that's nearby, and then contract as she approaches it?
The solution involves both tried-and-true raycasting and some inverse kinematics. While daunting at first, IK math is pretty simple - although I admit that I forget what some of it means after writing it.
To start off, I created an Area2D centered at the character's shoulders which extends outward. This represents the area where intersecting walls will be considered as candidates for reaching out toward.
Then I iterate over the colliding walls, looking for the closest one. To measure closeness, I have a naive method I'm still improving - presently I cast a ray toward the center of the wall and compare intersection distances. In reality, the closest point on a wall is probably not on the line from the center of the wall object to the center of the character. I'll probably improve on this later, maybe by casting multiple rays at different angles.
With the closest point in hand, I send it off in a signal. Other nodes can listen for that signal and update their understanding of where the arms should 'reach toward.'
And that's what the Skeleton node currently does. I just loaded all my animation logic into a script attached to this node. Once it detects a close wall point, this script will attempt to reach the arm toward that point, stopping at the fully extended length of the arm. Then it's a matter of computing the angle of the elbow so that the entire forearm and upper arm lengths are accounted for, forming a triangle.
Once I had that logic down, I generalized it and added an Area2D for the legs as well. This one is shaped differently, focusing only on walls below the character. Now, when the character is floating in space, both her arms and legs will brace against objects she collides with!
That was the first bit of procedural animation, and it was a rush to get it working so quickly! Moving on, I turned my focus to the gravity gameplay. Since I'm going all-in on procedural animations, I now needed a run animation in code.
Recalling the video, I began by implementing a basic wheel which will update its rotation based on the character's velocity (i.e. converting linear velocity to angular) and output a percentage of rotation which represents the progress of the looping run animation.
Now I needed to translate that rotation into some sort of running motion. After trying some more naive options, I had the idea to just try modeling the foot position with an ellipse and using inverse kinematics to adjust the position of the rest of the leg. This actually proved to be a fairly good approximation of locomotion, albeit pretty stiff. I change the shape of the ellipse based on the velocity of the movement - faster movement produces a wider and taller ellipse which is higher above the ground; slower movement follows a smaller one which is close to the ground. Adding that touch started to make the movement feel more natural.
Following that up, I created a similar elliptical system for the arms, tweaking the velocity adjustments to move the hands downward as the motion slows.
The result was... not amazing, but not bad for such a naive algorithm! The arms are kind of... flailing. But it's a good start.
I also created a new script which I attach to each Bone2D node. This script is an abstraction for combining multiple animation influences on a bone each frame. I figured there may be an arbitrary number of animations applied to any particular bone - for instance, if the character is running toward a wall, it might be cool if she reaches toward the wall to brace her impact using the code I wrote for reaching in zero-G. I would need to determine rules for combining these animation influences, and a dedicated abstraction is a good place to do that. So I wrote an API for this script which allows me to add rotation influences (with weights) each frame, and then at the end of the frame it 'flushes' the rotations, combining them based on weight, and applies that to the bone.
That's it for the moment - but I'm pretty happy with the progress on procedural animations. I feel like I'm coming up with some reasonable abstractions which should help me continue to iterate quickly.