I’ve been pretty quiet the last few weeks. I finished up my entry into a game jam, and I’m happy overall with what I made, but I’ve been a little lost in my head the last couple weeks since the end of the jam.
- Keyframe-based animation system
- Ability to remove Components from the Scene
- Refactored high-dpi support to Rust’s type system to ensure math is always done on the same type of measurements
- Various small improvements to the API
Overall, I was pretty pleased with using my engine. That being said, there were several things that I want to dive into and eventually change/replace.
Component Communication Guarantees
As I was working on synchronizing timing information between the parent “game” component and the children elements, I quickly realized that there wasn’t a great way to ensure that all of the components agreed-upon state during the same “frame” without using a shared handle of some sort.
This stems from how the methods
receive_message are invoked. They are called directly before the component’s
update() method is called. This iteration is done from the parent node “downward,” which means if you try to send a message from the child component to the parent component, that message will not be received until the next frame.
I have several ideas on how to solve this, and some new ones based on more recent work I’ll discuss below. Ultimately, I opted to share a handle between the game and child components for the jam to avoid needing to solve this problem during the jam.
For a lot of games and applications, this issue is not a problem at all. But for a timing-based rhythm game, on a 60hz monitor, each frame takes 16 milliseconds, and that delay of a single frame would need to be accounted for in the code that determines if a click is successful for not.
Related to the communication guarantees, I realized that inside of the
receive_message methods the context provided is a basic
Context. Some operations need the Scene to succeed, and thus you need a
SceneContext. I had a situation where I wanted to call a method both from
update() and from
receive_input(), but I needed the Scene in the method I was calling.
I have a love/hate relationship with how I’ve designed the contexts for the Component methods. The goal of the context is to allow me to stuff a lot of functionality and information in a single parameter so that the component method definitions are short and readable.
The list of contexts are:
- Context: Base context. It can be used for communicating with other components, spawning new components, updating styles, etc.
- SceneContext: Wraps a Context and adds a SceneTarget. This gives you access to the
- StyledContext: Wraps a SceneContext and adds an EffectiveStyle. This context is passed into layout related methods, where style information such as font size and family could affect component sizing.
- LayoutContext: Wraps a StyledContext and adds the ability to lookup your layout information as well as call methods like
content_size()on other components.
Overall I like this approach, but under the hood, the dirty little secret is that the Scene is always around. It’s shared between frames. There’s no reason for me not just to place the SceneTarget onto Context and move on… except that feels wrong.
Ultimately the real issue isn’t with Context/SceneContext. The real problem is that the Scene type has way too much responsibility. I don’t want API consumers to be able to call rendering methods except inside of the
render function. Yet, I need to provide access to the Scene in other methods because it’s also what contains information such as the viewport size and scale.
I’m going to be pondering splitting Scene between rendering functionality and information that doesn’t change mid-frame.
For a system that I whipped up in a single day (barring a few bug fixes on subsequent days), I’m pretty happy with how it turned out. Here’s a snippet of enqueuing a new keyframe:
self.manager.push_frame( self.image.animate().alpha(target_opacity, LinearTransition), completion_time, );
The current system is very… “type forward.” After spending the day architecting the way the animation system would work, I realized that this approach is “fine,” but not as flexible as I was hoping. The current system requires that you either have a single “AnimationManager” for each animateable property, or you have some gnarly type definitions. You can only push frames that contain every property.
I’m not quite sure where I want to take this system. I was hoping to create a system that was easy as my memory of jQuery’s APIs like
On the one hand, I could extend the current system and make some of the syntactical downsides more palatable. Still, on the other hand, I am curious about exploring a rewrite where I centrally manage animations and enable an API closer to jQuery’s.
Timing is hard
This isn’t related to Kludgine itself, but rather the game jam entry. I’m not happy at all with my approach to tracking beats. There’s a couple of known issues related to it. Ultimately the problem is this: I need to play a track and synchronize animations to beats relative to the start of the playback of the audio.
For the animation system, I need to queue up keyframes to animate the opacity and the individual frames of animation. Each time I get a
SetBeat command, I check to see if the current beat that I’ve already animated has passed and if so, I queue up the animation to the next beat.
The player clicking, however, might click after the beat. So, even though I’ve gotten rid of the “current beat” for animation purposes, I need to track that beat still to make sure the player clicks it, or count it against their progress if they miss the beat.
Hopefully, you can understand from those edge cases why this isn’t overly straightforward. I spent a lot of time trying to fix the current approach, but ultimately I think I need to revisit the design entirely (and add unit tests).
Figuring out where to go next.
As I was wrapping up the game jam, I started thinking, “what next?” For those who have been following along, I’ve gone through a lot of differing ideas, and sadly I’m having trouble deciding what interests me the most.
Do I want to make one big MMO with a complex player-driven economy and put all my eggs in one basket? That’s the general idea I “quit my day job” to pursue. Do I want to make a casual MMO more akin to Animal Crossing? That’s the idea that I realized I also have a lot of passion around – designing something to help people unwind and socialize. Or, do I want to spend some time making some smaller multiplayer games that aren’t MMOs, and by that virtue will ship sooner? Or, do I want to try one of my website ideas and not do a game right now?
I can say with certainty that I am most enamored with the idea of creating one game and pouring my life and soul into it rather than creating a series of games. But, that is also the path that takes the longest to see real fulfillment.
For the first week after the game jam, I didn’t do much as I pondered what interested me the most. One thing I did decide was that I wanted to not only dive into music composition more but also into art more. That’s where the idea of working on smaller projects appeals, but I also could argue that I can always redo early assets if I worked on a long-term project. Either way, this decision is that my twitch stream, once I get back into streaming, will be a blend of not just me coding, but my pursuits of art, music, and creative writing as well.
Actual changes for Kludgine
While I didn’t do much, I did complete one exciting project. During my reflections, I found myself excited by the prospect of developing my UI library. Even if I don’t end up succeeding in game development, I would love for a performant cross-platform Rust-based UI framework that I enjoyed using for other projects.
This project was one of the last steps to building that solid foundation. A fun little project I was thinking of tackling would be a tool similar to Alfred but cross-platform using Kludgine. That sort of app runs in the background 24/7. You do not want apps that run all day to continually trickle at your CPU.
That’s what the project linked above implements. Most of the examples in the repository will now use 0% CPU unless you interact with the window in some fashion. Even then, it attempts to avoid redrawing the screen until at least one component asks to be redrawn, or the OS asks for the window to be redrawn.
If you use an animated Image component, it will automatically request to be redrawn at the proper intervals to minimize CPU usage. Similarly, the animation engine will request a fixed frequency of updates while there are pending keyframes, but once no animations are being performed, it will stop requesting refreshes.
I have not yet updated the TileMap to support this. Right now, TileMap is not a Component, and it really should get turned into a Component. Once it’s a component, it will be trivial to implement the same reduced CPU usage behavior.
If you want your window to have a fixed refresh rate, you can implement
Window::target_fps(&self) -> Option<u16>.
I’m hoping another weekend of reflection will help me finalize my decision on what long-term project to develop. In terms of Kludgine, I have one big project I’m considering tackling, but aside from that, I want to get back into some more UI support – namely grid-based layouts, “modal” support, and scrolling. Part of that is a desire to start building some synthesizer editing UIs for muse.