[UPDATE 1 on April 7: Added paragraph about effects and tweens.]
I am still working on the Flutter-powered game that I mentioned before, although I had to put it down for about three weeks while I worked on a departmental report. This past week, I was able to get back into working on other things, and I was somewhat surprised to see Ludum Dare 57 coming up. I am glad the organizer was able to get the support he needed to run the event. On Friday, before the jam started, I spent several hours tinkering with Flame. It is a Flutter-powered game engine that I have known about for some time but, until Friday, had never tinkered with. I pieced together a minimal arcade-style interaction following my usual inspiration: Every Extend. It was enough of an exploration that I figured I could try using Flame for Ludum Dare, even though I knew it would be slow going.
At 9PM Friday, the theme was announced: depths. I sat with a paper and pencil and started doing some doodling. As I drew out some screens for a rather silly concept for a fishing game, into my mind came the Bonzo Dog Band's "Straight From My Heart." The juxtaposition of these two tickled my fancy, and I decided to go with it.
My hope was to spend most of Saturday on the game and then be done with it. Alas, it was a lot slower going than I had anticipated: I ended up spending all Saturday and much of Sunday on the project, and I still didn't get to some of the juice that I had thought of as essential for the experience. Of course, if the goal was to make the game as well as I could, I would have used Godot Engine. My goal, instead, was to learn as much Flame as I could to finish a project within a 48-hour window, and that, I did.
You can check out Fish Face on its Ludum Dare page or go directly to the web-playable version. The source code is on GitHub. The title of the game is a bit on the nose, and I had originally wanted the entire experience to be more surreal. I would have then given it a name like It's a baseball cap on top of an umbrella, but maybe I can save that for my next surrealist project.
Before I got into the technical implementation, let me say a little about the music, which may be my favorite part. I used to do more songwriting, but these days, I only ever eke something out during a game jam. A student pointed me toward some different SoundFont files, and I had downloaded a few last week to tinker with. That tinkering came during Ludum Dare 57, when I adapted the chord progression, transposed, from "Straight From My Heart" into a doo-wop ballad. This arrangement entirely used the Arachno soundfont. Curiously, in my head, this was the actual instrumentation of the Bonzos as well, and it was only after going to the recording that I realized it's primarily guitar, bass, and drums, with just a little sax and then, later, Hammond organ. It was intentional though that my arrangement could be played by the Bonzos. I hope Neil Innes doesn't mind the rhythmic eighth notes the piano got stuck with. In any case, I was really happy to arrange something with a diminished chord, an augmented chord, and modulation.
As for Flame, I will share my experience as some observations about "pros and cons." Keep in mind that this is my first experience with it. Much of the friction undeniably comes from my trying to think about the problem in the way the Flame architects intend it to be used. My intuition at this point is to approach it the way that Godot Engine would handle it, but while there are similarities, it is not the same.
Benefits of Flame
Flame uses the composition of components to build a scene tree in a manner similar to Godot Engine's nodes. This made it easy for me to design a state machine for the main gameplay following the classic State design pattern. I made an abstract State class that was itself a component, and I made my GameWorld have only one of these instances at a time. By mounting the current state as a child component of the GameWorld, its methods were automatically called by the system without my having to delegate from GameWorld. For example, the individual state's onLoad and update methods were called by virtue of its being in the component hierarchy.
One particularly useful type of component is
Effect, which can be used to add
all sorts of useful transformations. Progression through the transformation is handled by an
EffectHandler, so doing something common like an easing function is done with a
CurvedEffectHandler that uses an ease-in curve. Effects provided an easy way to get simple animations in place. Running effects in parallel was a simple matter of adding multiple effects, and running them in series was made easy with
SequenceEffect. I especially liked
RemoveEffect as well, which comes at the end of my animations that toss face parts off of the screen. It wasn't until after the jam that I realized that Flame's effects fill the same niche as Godot's
Tweens: you can stick an arbitrary effect on an object and then forget about it or, if needed, get a notification when complete. Comparing the two shows an interesting difference: Tweens can operate on any property because GDScript is interpreted, while Effects have the benefit of static typing and clear factory constructor names.
Individual components can be augmented through
mixins. Dart is the first language I have used significantly that incorporates mixins, and I feel like they are not yet in my arsenal as a software designer. Flame gave me a good excuse to see them in action. For example, my
CatchState is where the player must either keep or toss a face part. By giving it the
KeyboardEvents mixin, this state now can implement
onKeyEvent. I like how explicit this is and how it prevents the superclass from having more methods than necessary. I need to think more about whether this is better or worse than an approach like Godot's, where any node can process input.
Mixins are also used for dependency injection via HasGameRef. This mixin makes the corresponding class have access to a reference to the game, which prevents one from either having a global variable (with the corresponding spaghetti) or passing an explicit reference around all the time (with the corresponding noise). It makes me wonder if there are places in my Flutter game project where my code would be made simpler by using mixins of my own devising, and this inspires me to dig more into the literature on best uses of mixins.
As I have mentioned before, refactoring is comfortable and seamless in a statically-typed language with a powerful IDE. Easy renaming is so important that it should be a presumption of an environment Anything else introduces friction into the already difficult problem of evolving good designs. Similarly, Dart's library-based privacy is convenient for rapid development and code evolution. I can quickly add new classes to one file, and I can keep these private to that file. Classes can then be pulled out into other files as needed.
Flame allows one to use Flutter for UI elements, which should be a greater strength given the power of Flutter and the frustrations of UI work, but this strength was not demonstrated well in Fish Face. I came into the project with a backwards understanding, thinking of Flutter UIs as being added to a Flame game, but I think now it's the other way around: you add a Flame game to a Flutter app. Honestly, I am still having trouble figuring out what this means in terms of design trade-offs and best practices. There was a very helpful chap on the Flame Discord who pointed me in the right direction, but I still feel like I don't have a good understanding of how, where, and when to bring these two worlds together. I revisit this in my commentary about Flutter below.
One of the advantages of being in Dart is that you have all of
pub.dev at your disposal. Adding or removing dependencies is a breeze.
Building for the Web was mostly seamless. I had one case where my code worked locally and failed in production, and I am not sure what the root cause was. I got around it by changing my implementation of randomized selection from a list. I should probably see if I can reproduce the situation in a smaller example and report the bug. Otherwise, though, it was very easy to keep the game running locally as I worked on it, in traditional Flutter Web fashion, as long as I wasn't changing assets or dependencies.
Challenges of Flame
Parts of the reference documentation are helpful, such as the list of effects, but there are other parts that seem to assume that the reader is already approaching the problem the right way. This is a bit hard to quantify, but I think what I was looking for is something more like the Flutter Cookbook: not a tutorial for beginners and not reference material for regulars, but something in between, that deals with common problems and their idiolectic solutions. In part, I fear that the Flame developers are hamstrung by the excellent documentation and examples provided by the Dart and Flutter teams. There is no way for a small project to keep up.
My vision for
Fish Face was that there would be a magenta background with random shapes falling down the screen. This is the job of a particle system, but I could not make any headway with Flame's particle system; the best I got was a single particle. The documentation declares how robust their system of particle composition is, but I found the examples lacking for simple use cases. I spent a little time online looking for other tutorials, but I was running out of steam and this was low-priority juice. It was particularly frustrating because I knew how very easy it would have been to use Godot's particle system, to add it and interactively tweak it until it was good enough. This was a case where Flutter's code-based approach was much slower than turning a few knobs in a UI. Fortunately,
Flutter is getting a widget preview system akin to Godot's "Run Current Scene" feature. I am heartily looking forward to how this will benefit my Flutter workflow. Thanks to the Discord denizen who pointed me toward this upcoming feature. (Incidentally, reading about Flame's particle system reminds me of the time I tried to learn the very basics of
Niagara. I think there's a similar kind of shift in perspective that's required here. It's where I become tempted to do a deep dive and create a tutorial video to help others caught between worlds.)
When I sketched in the rough arrows of
Fish Face's interface, my intention was that I could easily recolor them in-engine for animated effects. In Godot Engine, I would use a shader to do this, and I assumed Flame would make this as easy. Alas,
the only mention of shaders in the Flame documentation says that they are coming later, once Flutter supports shaders for the Web. I don't understand why Godot Engine has shaders that run fine on the Web but Flutter and Flame don't, but it's probably because the Web is kind of an awful platform. I played with Flame's tint feature as a way to simulate what I wanted, but it was tinting not just my background, but the black outlines as well; that is, it tinted the whole image, not selectively the way I intended. I ended up just copying and pasting the images and manually coloring them in Gimp, then importing them as new assets, like a barbarian.
I enjoy programming in Dart and Flutter, but I still don't have a good handle on Flutter's animation systems. I have built a few demos but never really developed something that helps me internalize how to use
implicit animations for juice. I have a sense that there's untapped potential there and that my ignorance is the impediment. Indeed, one of the things I'm hoping to tackle this summer is to make my Flutter-powered, TTRPG-inspired game more delightful by incorporating some UI animations. I realized too late into
Fish Face that this exploration would be taking me in the wrong direction. In retrospect, I could have built the whole game just in Flutter, without Flame, and learned more of what I wanted to learn. There's a mismatch between the game design and what I was hoping to get out of the experience, but it's possible that I could only know this after having made this mistake.
Wrapping Up
It was a fun weekend with Flame, and I feel like I have a much better understanding now of when I would pick it up again—and it would not be for
Fish Face 2: Umbrella Hat. Part of me wants to return to my
Every Extend tech demo since that would be a much better use case for it.
The Flame Jam is coming up later this month, but it's not a good time for me to another side project due to the end of semester and family obligations. Spending the weekend with Dart makes me eager to make some more progress with my Flutter-powered side project even though I still haven't played with the latest Godot Engine release.
No comments:
Post a Comment