My family has continued to pick a day each month for a Fam Jam, although I have not written a blog post about it since June. Yesterday, we completed the December entry, Find the Ornaments. The ten-year-old was the Creative Director, and over breakfast he expressed his idea primarily in terms of mechanics: he wanted an endless runner with collectibles. His original sketch included sharks and water, but our discussion turned it into a more holiday-themed game. Whereas some of the more recent games have been fairly straightforward in their implementations, this one required my learning some new tricks, and so it seems like the right time to gather some learnings here on the blog.
We knew we wanted to do long jump or short jumps depending on how long the jump button was held down, but neither I nor my 13-year-old had ever programmed this before. We started by trying to simply puzzle it out from first principles, but when it become clear that this was trickier than we initially expected, we turned to the 2D Platformer Demo for inspiration. The code here is strange, as it contains what should be good ideas (like functional decomposition) but expressed in such a way that make it very hard to follow. For example, at one point, the instance variable _velocity
is assigned the result of calling calculate_move_velocity
, sending _velocity
itself as a parameter. Within the implementation of that method, the formal parameter name becomes linear_velocity
, while there is local variable velocity
that is used to build a solution. So, we use the velocity to compute the move velocity by sending the velocity to the linear velocity which returns the velocity. Not ideal.
I was also puzzled by the fact that I could not figure out where gravity was being applied. Clearly, something had to be pulling downward on the kinematic body of the player character, but where was it? My son pointed out that it was in the superclass script: it had a one-line _physics_process
implementation that adjusted _velocity.y
downward by gravity * delta
. I pointed out that this could not be the code that was doing it because the Player class' implementation of _physics_process
was not calling the superclass implementation. He looked puzzled and repeated that the superclass implementation was doing it, and so that was doing it. After searching online, I discovered that this is explained in the documentation, at the end of the "Inheritance" section, but not with anything like enough of a caveat as a seasoned OO programmer would expect. It turns out that overriding a method has two different semantics depending on what is overridden: if the method is an engine callback, then all superclass' implementations are called automatically; if the method is a custom method, then superclass methods are not called unless invoked explicitly in the subclass using dot notation. That is weird. It boggles my mind to think that the GDScript designers thought this was a good idea. I also wonder what kind of expectations it builds in the minds of people like my son who program primarily in GDScript but who will likely explore other languages later.
Coming back to the problem of short and long jumps, once I got my head around the semantics and the awkward program structure, I was able to see the pattern in how the demo solved the problem. It is an interesting inversion of my expectations. In our first, from-scratch implementation, I had been thinking of the character as going up while the jump button is pressed, but to a determined zenith, such that releasing the jump button would result in the zenith's coming sooner. The demo's approach was rather to launch the player at a predetermined speed upward, and then if the jump button is released, to apply a one-time decay to the upward velocity. This approach is much simpler than what I had considered because it fits neatly into the idea that each actor manages its velocity, rather than trying to determine a target location and solving for the necessary velocity on each tick. We implemented this approach and it worked great.
In between programming tasks, I worked with another son on the soundtrack. My wife had encouraged him to try transcribing one of the Christmas songs from his piano book into LMMS rather than starting from scratch. He gave me a thumb drive with the result, and I had to laugh. I assumed he had made grievous errors in transcription, so I sidled up next to him and the laptop to review and help him see what he did wrong... but aside from lacking measure alignments, it looked correct! That is, the phrases drifted from the beginnings of measures, in part because of a discussion earlier that morning, when I encouraged him in his own compositions to think about pauses between phrases, but the pitches of "Jingle Bells" actually looked fine. Puzzled, we started a new project and put in the opening phrase using a different sample, and this worked fine. The problem turned out to be the e_piano_accord01 sample that he was using, not the transcription. Now that's unexpected! This morning, I searched a bit and found a discussion on the LMMS forums in which it is pointed out that several samples have the wrong pitches. This actually helps explain some of the other audio strangeness in his past compositions: he might not have the sensitivity to know when he doesn't have just dissonance but actual out-of-tune samples... and I'm pretty sure e_piano_accord01 has been one of his go-to samples. I am not sure the best way to help him out with this on future projects. I could print that discussion and ask him not to use those particular samples, or perhaps I could download for him a MIDI soundfont and encourage him to use that instead.
The next major programming challenge was to implement the generation of tiles for endless running. We had made an endless runner before, but it had a simple, continuous ground with randomly generated obstacles; that approach would not work when making sequences of ground tiles that can have holes in them. For yesterday's jam, my son started on the feature by implementing a timer that would dump out tiles at a fixed interval, but I did not care for this implementation because this interval would then have undocumented dependencies on the speed of the character and the difficulty of the level. He had a Zoom meeting to attend, so I took this over and redesigned it, but I stumbled onto some other missteps as well. These were rooted in the fact that, from the beginning, we decided to have the character static and the tiles move past him; the more I worked with this, the more troublesome it was, in part because the math was all based on trickery. That is, in the narrative of the game, the elf is running forward, but in the code, we had the world moving past him. The first major improvement I was able to make that I felt good about, then, was to change that around and have the elf move forward in game space. This allowed me to align generated tiles with specific, integer coordinates instead of pulling shenanigans with floats and deltas.
To generate tiles lazily, I remembered a technique from a UE4 Endless Runner tutorial series in which triggers were used on each tile to tell the game to generate a further tile. This is a nice approach since it should scale indefinitely, or at least until the representational limits of floating-point coordinates. It is the approach that I took then. The logic for handling passing these tile-triggers was also easily able to be repurposed for the generation of ornaments.
The next technical challenge I faced was to place the collected ornaments onto the Christmas tree at the end of the game. I drew the outline of the tree as an irregular quadrilateral using a hidden Polygon2D. Then, the problem was to generate a random point inside this polygon. I could neither find nor think of a clever way to do this, but a clear brute-force method appeared before me: generate random points anywhere on the screen until one happens to fall in the tree's bounds. This simplifies the problem to one of determining whether a given point is within a polygon. Surely, there is an easy way to do that in the engine, right? Every search I did took me to discussions like this one, where the answer was either to do an awkward snapshot of the physics engine state space or implement a complete raycasting algorithm. I was quite frustrated and shouted out more than once that we should just do something different and easier. Near the end of my rope, I did something like this at a time when my son, working nearby in my office, had finished his own train of thought, and he said, "There is a method to determine if there's a point in a triangle." My response was, I believe, "What." He insisted he had seen it, and once we looked together, we found the Geometry class, which contains the method point_is_inside_triangle
and, more importantly, is_point_in_polygon
. (Note again the awkwardness of inconsistent naming conventions!) Of course, these methods do exactly what I needed, so that the hour of working on this feature only actually took five minutes of meaningful effort. What really puzzles me, though, is why searching for any related combination of words fails to find any reference to these useful utility methods. Maybe I can break ten readers on this blog post and help some other poor folks who end up in my situation.
Those were all the interesting technical problems that arose in the development of Find the Ornaments yesterday. It is always frustrating to spend more time on a feature than one thinks it merits, but on the other hand, each such adventure leads to more tools on the toolbelt. For example, when working on the parallax scrolling yesterday, I started by just trying to remember the right combination of settings, but then it was much easier to just pull up The Flying Planes from two months ago and see how we did it. Incidentally, if you do take a look at that game and this game, and compare them to some of our older efforts, you'll see some of the amazing tricks my wife has learned in Inkscape to make beautiful vector graphics backgrounds.
Thanks for reading! I hope you enjoy the game.
You know my level of expertise, so can appreciate that I truly marvel at the detail of the post and the technical points of conversation between father and son. I did have to stop and reread the statement “He has a Zoom meeting to attend...” and chuckle at that thought. Thanks again for the peek into the grand world of game design. Now, about that iPad version LOL
ReplyDeleteFather and SONS
Delete