Showing posts with label gamedev. Show all posts
Showing posts with label gamedev. Show all posts

Monday, February 2, 2026

Shove Off! at Global Game Jam 2026

This past weekend, I participated in Global Game Jam at Ball State University. This was the first time in many years that I was not hosting the event, and it was a nice change to simply jam and not have to worry about logistics.

My team created Shove Off!, a local-multiplayer arcade game. The source and binaries are also available on GitHub. The game is for four players with game controllers.

Title Screen

My son and I started kicking around ideas right after the theme announcement. We briefly discussed making a survivors-like where the powers come from masks, but we tossed it as requiring too much balancing. We pivoted to using masks of world cultures to provide power-ups in a platformer brawler game. This idea played well into our observation that local multiplayer games can generate a lot of fun even if they are rough around the edges. Choosing this to go forward, we then decided on pushing as the core player interaction would be such that masks modify it. (Incidentally, I have played Smash Bros. exactly once, and my son has not played it at all.) Our whiteboard designs revealed an opportunity for two kinds of attacks: a horizontal push from the ground and a diagonal push from the air. The simple level layout we sketched provided all we needed for a playground.

We tried to get the core gameplay working Friday night, but we could not get all the pieces together. Crucially, we did figure out the sizes and shapes that everything would need to be. We also recruited a musician who agreed to write a retro-style, high-energy song together with sfxr-style sound effects for us.

Saturday morning, we got the core gameplay and representative placeholder art in place; that is, we replaced the Godot icon with stick figures. We enjoyed the core gameplay even without powerups. Knowing we were a small team with limited time, we decided to try to build a complete game using only the core gameplay. If it was good, it could be what we shipped, and if we had time, we could add powerups. During the day, the theme became less generic and focused more on lucha libre. Masked wrestlers pushing each other off of platforms seemed the right way to go. Once the musician's main theme landed, it changed everything: the song was perfect for the madcap gameplay and bold visuals we had brought together.

My youngest son was the only one with nothing scheduled Saturday afternoon, so I invited him to come hang out at the jam. I set him on some research tasks, including finding fonts and figuring out a palette for the characters. He started by simply poking around Google Fonts, but then I taught him about the value of using image search for reference images. We brought up images of luchador posters, which changed his whole approach. It was a good learning opportunity, and I'm glad he came along, even if he had to spend some time awaiting assignments.

Starting the match

Many of the posters we reviewed featured a starburst background, and it looked to me like something I could do with a shader. I tried puzzling it out myself but did not make much progress. Shaders are interesting to me but I frequently get stymied by them. I ended up searching the Web and finding exactly what I was looking for. Sunday morning, I added my favorite subtle feature: at the end of the game, the background color changes to match the thematic color of the winner. One of the other jammers required shader cleverness in his game, and I told him how I'd love to lead a seminar on the Book of Shaders. He said he'd be interested, so I guess I only need nine more undergraduates who are intrigued by this intersection of art, math, and design.

I should mention that all of the visuals were done by my other son who was in attendance. He did a great job iterating on ideas and taking feedback, reworking the characters several times as we worked towards our goal. One of the best things we did in terms of the project architecture was to separate the body animations into their own scene so that he could work on those independently while I worked in nearby systems. We only had one merge conflict during the whole weekend, and it was quickly resolved.

By Sunday morning, we had already covered the "mask" theme with our luchadores, but we had time to build on the core gameplay. Our testing showed that players could get stuck in pushing matches, and so we pulled our favorite powerup from Friday night's listing: fireballs. Every few seconds, a stylized sun shows up in the middle of the screen, and the player to grab it gets three fireballs. These proved perfect for breaking up the gameplay.

We were all happy with how the game turned out, and it was popular during the post-jam party. I felt good about making a complete, playable, and enjoyable game. I have spent a lot of time the past several months in preproduction and exploring engineering practices, but I haven't shipped anything in a while. The jam was a great opportunity to go from nothing to something in 48 hours.

If you try out the game, I hope you'll let us know what you think.

Monday, October 6, 2025

Experiences interacting with generative AI in my solo game development work

I have been reflecting on the role of generative AI in education. In order to better understand it, I decided to incorporate some generative AI systems into my software development work the last two weeks. To be clear, I had no great intrinsic motivation nor external pressure to do so; I mention this because it seems that most of the stories I hear about using generative AI involve either one or the other. Instead, I approached it from a position of curiosity while also trying not think too hard about the immoral ways many models have been trained and the incredible amount of power required to drive the models.

For my context, I have been developing a game prototype using Dart and Flutter. Some of my earlier attempts at using Flutter for game development were hindered by my own floundering with software architectures: several times, I built prototypes only to have a constant desire to refactor due to code quality deficiencies. This was especially true when using TDD, with its emergent architectures and constant refactoring, which had me frequently doubting whether I was programming myself into a low-quality corner. I suppose this is the danger of doing TDD outside of Pair Programming.

I started by using ChatGPT as a sounding board to talk about some of my goals, including following the principles of Clean Code, good architectural separation, and robust tests. My first significant conundrum dealt with separation of layers while using a functional approach. That is, I had a World object that represented the immutable state of the world, and then I had an Adventure object that held a reference to the current World. Taking actions on the Adventure updated the World. The programming for this was made easy by using freezed. At the same time, interacting with the Adventure could generate events to which the UI needed to respond, and so I took a page out of egamebook and had the Adventure create a stream of events that the UI layer can listen to. Dealing with streams is something else that Dart makes quite easy. 

These two different systems created a conflict regarding how exactly an interaction should play out, especially when one interaction generated multiple events. I explained this situation to ChatGPT, and I was impressed by how it summarized the pros and cons of each of the different approaches. It concluded that I should just stop what I was doing and simplify to a purely functional Adventure object that only transforms World objects, and in doing so, returns a list of events. The generated text was unequivocal and, I daresay, correct. I think I needed someone to point out to me where I was trying to do two things at once, and that my own constraints--using an immutable data model--logically led to my needing to abandoning streaming events in favor of a purely functional Adventure object. It took some rebuilding, but once I was done, the separation between layers was much more clear.

Dealing with automated tests is another area where I was surprised by the high quality of ChatGPT's recommendations. I had a test file with a few hundred lines of code, and I had written some helper methods to try to keep the test reading fluently. Yet, I felt unsatisfied with it. I pasted that code wholesale into ChatGPT and asked it to explain to me why I was feeling uneasy with that code, given my architecture. It observed that my code was mixing too many responsibilities together and that this could be improved by creating a separate test harness. This would move all the domain references out of my tests and into the harness so that changes to the domain would only change the harness, not the expression of the tests. Once again, it took some typing to get the system rebuilt this way, but the result was much nicer. In fact, a few days later, I made a significant change to the representation of actors in the world: I did not have to make any changes to my test code, and my harness only required a few tweaks to make it keep working.

Some time later, I ran into another problem, the nature of which I don't exactly remember, and I found that ChatGPT has lost the entire history of our discussions around software architecture. I asked it about what happened, and it gave me the impression that yes, sometimes context is just lost. I briefly considered doing more research into figuring out what this meant and how to avoid it, but I decided I should explore a different approach: generative AI that is integrated into the IDE. Because I'm using Android Studio, the path of least resistance was to enable the Gemini integrations and to give it access to my project.

My experience using Gemini was more mixed. Using the chat interface, Gemini provided advice that got me out of another architectural blunder with its recommendation to separate my Adventure object into different services. For example, I created a CombatService, and then injected that into the Adventure. My first attempt at this was to make the CombatService modify World objects just like Adventure does, but after some frustrations and interactions, I received good advice to make it only deal with the logic of combat, not with the logic of state management. This meant that my unit tests for CombatService could make sure it was working correctly. I could also inject a mock CombatService into the Adventure when I needed to control the output for Adventure's unit tests. For example, an actor has a chance to hit its target combat, but that kind of randomness needs to be factored out of a well-behaved unit test; making a mock CombatService that responds with a hit result meant that I could test that Adventure handled hits correctly without binding the test to a random outcome. Gemini framed this as a good place to practice separating unit and integration tests, which also gave me a useful perspective on this system I was building.

Gemini was less useful in terms of practical code examples, in part because it seems to not have enough training on Dart code that uses freezed objects. More than once, it tried to explain to me how I should be changing my code to use different kinds of object construction that just don't work with freezed. The agent mode, which is clearly labeled as being in beta, was particularly unhelpful because of this. As a result, I have barely touched software agents despite having heard some promising things about them, such as in this interesting conversation between the hosts of the Mob Mentality Show.

My initial reaction to these systems is that they have been helpful for me because I can understand the implications of their recommendations. There were several times where the recommendations were clearly bad because they violated some particular principle, and when I pointed that out, the system was sycophantic about how smart I was for pointing out this problem, and then it changed direction, presenting the new advice with as much certitude as the last. This combination was troubling: confident answers and fawning reactions to criticism. It would be a terrible team member. Despite my overall positive experience, I am left uncertain about how much it was teaching me versus how much it was helping me stick to my guns and reminding me of how various principles arise in practice. Put another way, I retain a suspicion that I would be much more productive if I was pairing up with someone of equivalent experience and similar goals to mine. 

Figuring out what any of this means for students is a subject for another time.

Saturday, May 3, 2025

The outcomes of the 2024-2025 Game Production Studio

I have been working with a multidisciplinary cohort of students from Computer Science, Art, and Music in the Game Production Studio for the past three semesters, team-teaching the experience with Antonio Sanders from Animation. At the end of this process, we had three games released for free on itch.io:
I am proud of the work these students have done, but it was a very different experience from the previous cohort. Our third cohort of students in the Game Design & Development concentration are currently wrapping up preproduction, but I have not been involved in that course. I am out of the sequence for a year as Travis Faas steps up to lead the sequence, which frees me up for Fall's sabbatical that I mentioned yesterday.

One of the primary differences between last year's and this year's cohorts was scale. Mentoring a single team of six students is very different from mentoring 25 students on three teams. Some things I did intuitively needed to have been more structured. The clearest example is the asset pipeline. Both years, there was a studio policy that everyone needed to be able to open and edit assets and get them into the engine. It was easy to confirm this with my smaller team, and for the larger one, I took students' word for it. It became clearer to me late this Spring that a significant number of students could not, and never did, know how to do this: instead, they relied on manual hand-off procedures. The point here is that, with a small team, I could simply observe them in every step and know they had met expectations, but with the larger team, that did not scale. "Trust" and "verify" needed to be coupled, especially since it wasn't even clear that the students who had previously certified their compliance with the protocols understood what it was all about. Release engineering and quality assurance were two other areas, along with asset pipelines, where I know that I will need more formalized approval processes in the future. I don't know yet how much those processes will look like industrial ones, with some kind of threat of firing or demotion, or academic ones, wearing traditional pedagogic trappings.

Managing three simultaneous teams made it difficult to determine the timing of iterations. We ended up keeping all the students on the same schedule, which meant that there was one day when each team was doing review and retrospective meetings. One of the best decisions my co-teacher and I made was to have both of us sit in on the review meetings, and that's something I need to keep for next time. However, in reflecting on the semester, I think I should have also been there for the retrospective meetings, at least at the beginning of production. This would help me model the process for the students. Also, it made me reflect on how in my immersive learning classes, it was the retrospective—not the review—where I made a lot of my pedagogic insertions. Once a team identified a pain point, I could direct them toward an industry standard solution via just-in-time teaching. If I have another large section, I need to consider staggering their iterations so that I can be at both meetings.

We gave our students a short written final exam that was inspired by the questions we used during team retrospectives. I took notes about their responses to the final question, which was to identify something that they would do differently during production, knowing what they know now. I consider this one of the most important outcomes of the entire three semester process, and so I will summarize their responses here. I have clustered their responses and removed any identifying information, of course.

Work management. We used HacknPlan to manage work, but each team had slightly different stories about how this didn't go as well as they would have liked. Several students commented that using physical, visible task tracking was far preferable. It makes me want to bring in the same kind of task boards that I used in RB368 for my immersive learning classes. How to do this with equitably in a shared lab is an issue to figure out.

Scope management. Several students commented on how their scope was not well under control. I have been thinking about this as well, especially since I recently read Vasco Duarte's No Estimates book, in which he advocates for having proximal work in higher focus than distal work. This is somewhat contrary to the advice given by Lemarchand in Playful Production Process, which is our reference text for the studio. Lemarchand advocates for having a full production plan at the end of preproduction, but at the same time, he recommends breaking down a production phase into iterations—without really explaining how one might do that. I think Duarte's model is going to be more helpful here, and I'm thinking about abandoning the entire "macro chart" element of preproduction in favor of epic stories.

Code quality. A few students commented on how they needed to have better control over their technical implementations. Some mentioned a bad habit of pulling the first tutorial they could find into their production code, and how this would lead to an unmanageable mess of ideas. Others talked about how they should not have tried to reuse the preproduction code since it was too quick-and-dirty. A few recognized that introducing more automation would have saved them a lot of trouble, but also that their failure to refactor meant that their code was not amenable to testing. 

Iteration. Some students recognized that they needed more iteration on their core design loops and on their assets. Our students had a bad habit of taking an inspiration directly into production without low fidelity prototyping and experimentation. It was good to see them talk about how they should have done more sketching and playtesting. I do wish they had appended "as you instructed us to do" to their answer.

Vision management. One of the strangest things to me is that, despite my repeated suggestion and admonition, I don't think any team had a vision statement: they didn't articulate design goals or experience goals. This either caused or is a symptom of significant problems. Conversations about design alternatives became about force of personality rather than alignment with a vision. It was good to see that some of the students recognized this and highlighted the need to articulate vision as something they would do differently. It is the kind of thing that, in my producer hat, I can demand of future teams, and in my professor hat, stick a grade on so they know I am serious.

Marketing. Some students recognized that decisions made in preproduction lingered far longer than they should have. This is strongly related to the previous point about vision management. From my perspective, it looked like these students were on the road to recognize that they were rationalizing the past rather than being intentional about the future. I see this frequently with my students, that they will tell themselves a story about what their vision must have been because it aligns with what they already did. It's very human. It's also dangerous for collaborative creative work.

Specificity. This final pattern in the students' responses caught me by surprise, and I was delighted with how students articulated such an interesting problem. Many recognized that their discourse throughout the project was often at a level of abstractions but that the design and development work they needed to do was at the level of concretion. Again, I think the lack of clear design and experience goals contributed here, but it's also a case that makes me think about techniques like Stone Librande's one-page design documents. Looking back at the three projects my students made this year, each one of them could have been expressed in a one-page design and hung on the wall.

It was a trip working with these students for the past three semesters. They should all be proud of their accomplishments: making games is hard! Some of them really impressed me by learning new things, working through social and technical challenges, and devoting themselves to solving hard problems. I love teaching this kind of class, but it will also be good for me to take a little break. I look forward to what a sabbatical can do for my perspective on some of these ideas.

Monday, April 7, 2025

Ludum Dare 57: A Weekend with Flame

[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. 

Tuesday, February 11, 2025

Notes from "No Estimates"

I recently finished reading Vasco Duarte's No Estimates after hearing about it on Dave Farley's Continuous Delivery YouTube channel and Allen Holub's #NoEstimates talk. I had been curious about the #NoEstimates movement for some time, reading an article here and there, but this was my first real attempt to understand it. The book itself is clear and direct, interleaving traditional content with an ongoing fictional narrative that motivates and reinforces the ideas. I found many connections to my research and teaching interests. In this blog post, I will share a few findings from my notes and reflections.

Estimates

One of the foundational principles of the book is fairly simple but not something I had considered before: an estimate communicates the peak of a probability distribution. For example, if I estimate a task to take two hours, I am saying that the most likely case is two hours, but it could take as little as zero or negligible time, but it could also take a low probability that it takes forever. The cumulative probability is the area under the curve. From this, we can conclude that the probability of being late is much higher than of being early.

Duarte classifies estimates as waste as defined by a Lean perspective, that more of it will not make the product better from a user's point of view. It clarifies that "no estimates" isn't a goal but a vision: estimates won't be axiomatically removed but minimized. The discussion of waste got me thinking about QA testing in games, a point I will return to below.

Managing time, scope, cost, and quality

I regularly talk to my students about how cost, quality, and scope are the three levers we can control in project management. Since our work is constrained by the semester's schedule, I point out that we cannot shift cost; that is, we cannot simply add more time to the end of the semester to be able to get our projects done. I also argue that quality is non-negotiable: the point of undergraduate education is to learn how to work well, so sacrificing that is against the telos of the endeavor. Therefore, the only manipulable lever is scope. This perspective seems to help students understand why we focus on user story analysis, prioritizing based on features that add value to the users. 

I wish I could remember where I encountered that heuristic since it is distinct from a similar concept that dominates Web searches: the project management triangle, which is also known as the triple-constraint model or the iron triangle. This model explains how the constraints of scope, cost, and time are connected such that cutting one without changing the others will result in a loss of quality. Duarte uses this model in his book to draw a distinction between value-driven and scope-driven projects. Traditional management approaches are scope-driven, where the scope is fixed and so cost and time are unbounded. Scope-driven projects instead fix time and cost, leaving scope flexible, which leads to the approach of delivering the most value first. This is a standard agile perspective, but I previously didn't have the nomenclature of "value-driven" and "scope-driven," perhaps in part because in my academic environment I rely on that alternative model described above.

Reducing variability in throughput

In Chapter 3, Duarte provides suggestions for techniques to reduce the variable in throughput for a development team. I have used many of them before in mentoring student teams. These include using stable, cross-functional teams teams; having clearly defined priorities; not passing defects down the line; standardizing and automating when possible; freezing scope within iterations; and protecting the team from outside interruptions. He also suggests reducing dependencies so that people can work on one thing at a time. This got me thinking about how often my teams end up with coupled user stories, such that completing one requires work on another. Creating independent user stories comes up more than once in the book, and it's something I can watch for opportunities to practice and teach.

Duarte points out that good requirements must allow measuring progress early and often. They must also be flexible enough to determine which aspects of a system need to be implemented now and which can be built later, after the system is better understood. 

This leads to Duarte's conclusion that the only real measurement of progress in software development is Running Tested Stories. Anything else is ambiguous or unreliable. Teams can be managed toward consistent throughput by ensuring that there are no large stories (no larger than half an iteration), several independent stories can be completed in an iteration, and the distribution of story size stays about the same throughout.

The book references a 2003 article by Bill Wake about the "INVEST" acronym, which I had not seen before. Wake describes how user stories need to be Independent, Negotiable, Valuable, Estimable, Small, and Testable. "Negotiable" here means that they deal with the essence and not the details: they are not contracts about technical details. Wake's definition of "Small" is between half a day's effort and a day's effort. Duarte adapts "Estimable" to be Essential, which is sensible given his specialization. He includes the term blink estimation, which he attributes to Angel Mednilla and was new to me. The idea is that one makes a snap judgement about whether a story fits within two weeks or not, and that this blink estimation is usually all that is needed. Regardless of which expansion I use, INVEST may be a helpful heuristic to give to teams who are breaking down a big problem such as a game design into smaller, valuable pieces.

Planning the details just in time

I started using Scrum with multidisciplinary undergraduate game development teams many years ago, and it has been a valuable practice. I was usually the Product Owner, responsible articulating the work as user stories and prioritizing the backlog. Teams pulled stories from the Product Backlog to the Sprint Backlog during our planning meetings, as per traditional Scrum. When my teams found that a one-dimensional Product Backlog makes it hard to see the big picture, we adopted Story Maps, which ameliorated the problems. Although we tracked each Sprint's progress using burndown charts, I never bothered to compute velocity. Teams tended to get a good sense of how much they could do in two weeks by around week ten, and since I was in charge of the backlog, I could cut scope to fit into the time remaining.

My preference for agility caused some friction then when trying to apply Richard Lemarchand's Playful Production Process with my last two cohorts of game development students. Although Lemarchand calls for concentric development, he has relatively little to say about how to implement that. More importantly, his approach for each phase of production is to start by enumerating all the work that is to be done, estimating how long it will take, and then moving toward that goal. A careful reader will recognize this as scope-driven management, and a cultural observer will note that the games industry is beset by death marches and crunch. 

Duarte's alternative is rooted in agile principles: plan the details of the imminent iteration, getting them into user stories that can be completed in a day or two, and let the future work remain coarse-grained epic stories. He suggests not planning more than about two months' worth of work due to how much will be learned about the system in that time. 

This caused some stress for me since it was quite counter to one of my ongoing research projects. I have been thinking about how to combine some of Lemarchand's ideas with some ideas I took from Allen Holub's #NoEstimates talk. One of Holub's primary arguments is that we can simplify our planning, and get equivalent results, by counting each user story as a single unit of work. I have been investigating the differences between tracking work items as single units versus tracking estimated hours remaining. For example, consider these two perspectives from the end of a team's alpha phase of production.

These are two perspectives of the same period of time, an Alpha phase that lasted about three months. The first shows the number of stories in the backlog and the second shows the total estimated number of hours. The top chart shows how the team cut a significant number of features around a third of the way through Alpha. For the next third, they added stories at about the same rate as they completed them, demonstrating how they were working to reshape the project based on the initial overestimate. We set up a nifty toolchain for tracking these data in realtime using a combination of Hacknplan and Google Sheets. I even gave a workshop about this at GDEX a few months ago. But the whole thing hinges on having those planning data at the end of August for a milestone that's coming up in early December.

Duarte suggests a radically different model. Break down the problem so that the stories for the current sprint are independent and small (taking no more than half a sprint to complete). Track how many of those the team can get done in an iteration. Do that for a few iterations, and you have a good sense of how much work the team can accomplish in future iterations, which lets you control scope. More specifically, you can measure a team's User Story Velocity and its Feature Velocity, where "Feature" here is elsewhere called an epic story or an activity. 

I like the sound of that. It was clear from watching student teams try to estimate an entire Alpha phase that they knew it was shoddy. Worse, for some of them, it planted a seed in their mind that they already knew enough about their projects to plan the whole phase when, in fact, they had yet to find the fun. Switching to the #NoEstimates approach would require me to supplement or replace Lemarchand's recommendations, including new ways of using project management tools.

The medium is the message

Incorporating #NoEstimates would also mean rethinking the relationship between the developers and the artists. When I was mentoring single-semester projects with a small art team, there was seldom any trouble: artists moved fluidly from concept art, sketches, and low-fidelity assets toward production-quality assets as the semester moved on. When artists struggled to match the iterative flow of the programmers, we adopted swimlanes as recommended by Clinton Keith

It wasn't clear to me how to scale this up to teams where half may be artists. I reached out to Duarte himself, and he was kind enough to talk with me about my questions. We had a fruitful discussion, and he helped me see something that I didn't understand before: there is a whole category of practices that, fundamentally, are symptoms of a failure to regularly integrate. Swimlanes are one example, but there are countless more, as overt as separate physical locations and as mundane as job titles and org charts. If we consider that the running tested story is the only way to measure progress, then anything that does not support that is potentially distracting from it. It is the kind of observation that would make Marshall MacLuhan smile: the presence of a swimlane says more about the team than anything in the swimlane itself.

Using social complexity to determine tactics

Buying the book also grants access to a keynote presentation that Duarte gave some years ago. It's an excellent talk and a good complement to the book. One particular element jumped off the screen and into my notebook, and that is Duarte's matrix for dealing with user stories. It deals with the problem of Social Complexity, which can be summarized as "the number of people in the organization you have to talk to about it." Here is a quick reproduction from his talk:


This captures something I have tried to express to many teams but failed to capture so clearly. It relates to the four conclusions of his presentation:
  1. Predict progress with #NoEstimates
  2. Break things down by value, not effort
  3. Agree on meaning with social and technical complexity, reducing risk
  4. Use RIDICULOUSLY SHORT timeboxes
He points out that if you can only do one of these, do the fourth one, since it is the essential practice from which the others derive.

Closing thoughts

I taught the first two cohorts of the game production sequence, but I am stepping away for the third one. There are a few reasons behind this, but primarily it's so that my new colleague has an opportunity to try his hand at it. I expect to be back in the saddle with the fourth cohort, who will start in Spring 2026. Writing up these notes took much longer than I expected, especially as I began to reflect on the substantial differences between what I have done in the past, what I did following Lemarchand, and what I might like to do in the future. For now, I need to put down this line of inquiry, but formalizing these notes gives me a point of entry when I need to refresh myself on these topics.

In the meantime, if you have thoughts, feedback, stories, or reflections, please feel free to share them in the comments.

Tuesday, December 24, 2024

Experimenting with software architectures for video games inspired by tabletop roleplaying games

I have been tinkering the last several months with a videogame prototype inspired by some of the tabletop roleplaying games that my boys and I have been playing. Similar to how my game The Endless Storm of Dagger Mountain explored PbtA mechanisms, I've wondered about the strengths and weaknesses of interpreting Forged in the Dark systems into a text-based videogame. Last week, once I put away most of the work of the Fall semester, I was able to dive more deeply into work on a prototype. I felt really good about it until a few days ago, when I came to doubt—not for the first time—some decisions I had made in the software architecture. So, in this, my sixth December blog post, I want to unpack some of the considerations that I have put into these efforts so that I might stop programming in circles.

I decided to use Dart and Flutter for the game. I teach with Dart and Flutter in CS222 because I legitimately enjoy the technology stack. I am competent with them but would not call myself an expert. I have only built two public systems with these tools: my Thunderstone Quest Randomizer and a little timer utility to help with Promotion and Tenure Committee meetings. The former is much larger than the latter, and if I were to build it again, I would do it differently, but I keep maintaining it for myself and other fans of the card game.

I appreciate Dart's static typing, named parameters, pattern matching, and sealed classes, and Flutter's declarative approach can simplify otherwise complex UI logic. Something else that draws me toward Dart and Flutter, besides the elegance of the language and framework, is the inspirational work of Filip Hracek. His Knights of San Francisco is similar to some of the experimentation I have been doing, and his writings about Flutter's performance and the ethics of software design are interesting and insightful. I spent most of a summer working through his open source egamebook repository, trying to understand how a serious Dart programmer uses the language to accomplish his game design goals.

However, the choice to use Dart and Flutter over Godot Engine is never fully settled in my heart of hearts. Whereas Dart is dreamy for game logic, Godot Engine makes it dead simple to create juicy bits of design. Its AnimationPlayer is brilliant for little effects, whereas setting up an AnimationBuilder  in Flutter takes a whole lot of typing. Godot's node-based approach means that individual parts of the program can easily be run in isolation and tested, and tool scripts allow customization of the editor itself. Unfortunately, GDScript has no refactoring support, and this is a significant impediment to a test-driven approach: changing my mind about a name or a design choice in GDScript has nasty rippling effects. Type hints in GDScript are invaluable, but they are no replacement for real static typing. Also, creating simple data structures in GDScript is much more arduous than in Dart. All this is to say that I'm dealing with game logic in GDScript, I find myself thinking, "This would be easier in Dart," and when I'm working on simple UI tweaks in Flutter, I think, "This would be easier in Godot Engine." I know that there's no silver bullet, yet I cannot silence the little fear that maybe I chose the wrong environment for this project.

State management is at the heart of any game software. The official Flutter documentation explains the basics, and the list of advanced options makes it clear that there is not one right way. I have long been intrigued by Bloc and decided to try using it as a state management solution for my experimentation. I spent a lot of time the past several weeks reading the official tutorials, and I believe I have a good sense of the system now. Crucial to Bloc is a separation of concerns: Flutter widgets provide a humble view of the UI state, which is managed in a bloc (business logic component), and this is separate yet from the domain layer. For Internet-connected apps, the domain layer involves a repository layer, but for my purposes, it was simple enough to roll these together. For my first Bloc-powered prototype, I followed the tutorials' approach and used equatable to generate some of the boilerplate required. Searching the Web reminded me of freezed, and once I understood how Bloc and equatable worked together, I happily switched to freezed for its excellent code generation support. Using the bloc and freezed snippets plugins for Android Studio is practically necessity here. Once my experimental coding was done, I felt like I could move forward with a more rigorous TDD approach, since now I could think about the features separately from the underlying architecture. I was inspired as well by Dave Farley's commentary about how a layered approach to unit tests means that developers can change their minds about implementation strategies without breaking all of their tests. Knowing that I would continue to change my mind as I explored the design space, I moved forward.

One of my early experiments explored whether I might just consider the whole game to be "business logic" that belongs in the bloc. That is, I considered cutting out the separate domain layer and putting all the game logic in the bloc. This was of limited viability as I quickly ran into two problems. One was that I found myself having to put game logic in the Flutter widgets since they could not simply read UI state from the bloc. This was clearly counter to the spirit of the architecture. The other problem came up when dealing with threat rolls. In the Deep Cuts rules expansion to Blades in the Dark, players roll dice and assign them to consequences, which are negative effects like taking damage or losing items. Assigning dice to consequences mitigates their impact. It struck me that assigning dice was purely UI state and not game state. That is, a player might experiment with different assignments of dice to consequences, but nothing in the game domain model actually changes until those arrangements are committed.

Armed with this realization, I extracted the game rules into their own module, and I gave this module its own immutable state. The state could be modified by a few public methods that were called by either the bloc or my unit tests. For example, the method commitDice took the assignment of dice to consequences and computed the resulting change in the game world state. This also let me separate that state from the widgets entirely: whereas I had been sending the world state to the Flutter widgets, now I could add a layer of abstraction related to UI state. For example, rather than sending the game world state to the view from the bloc, I could send only those details that mattered for the state, such as which buttons were enabled, or what text should be shown in a label. This meant I could have tests on the bloc and trust that a humble view would work as anticipated.

My pleasure at this transition made it even more disheartening when, earlier this week, I sat down to add a new feature and realized I had programmed myself into a corner. After assigning dice to consequences and before their effects are committed to the game world, a player can also opt to "push themselves" to mitigate consequences. This results in another dice roll whose outcome determines how much stress the pushing causes to the character. It means that between the committing of dice assignments and the final changes to the state is another step in which players might push themselves to alter the outcomes. However, this means that the changes to the world state might be coming from unmitigated consequences, dice-assignment-based mitigation, or pushing-based mitigation. The game world simply needs to change, but a good player experience in the UI should distinguish among these. 

A fair criticism at this point would be that I should have foreseen that pushing would require a more robust handling of actions and consequences. In fact, I was aware of this, but I was also trying to push the limits of narrow slicing and Farley-style TDD/BDD combined with emergent architecture. I wanted to complete a well-factored feature (in this case, dice assignment) before increasing the complexity by adding a new feature. Despite my efforts, I can see now that revising the core action resolution system will have significant ripple effects on my test layers.

Just before exploring the pushing mechanism, I had stubbed in an approach for dealing with the outcome of progress clock expiration. I needed to attach represent arbitrary game effects to a clock, and so I sketched in a Command pattern. In particular, I encapsulated the idea that the main clock would end the game by creating an EndGameEffect and attaching that to the clock. I used freezed for the Command objects to facilitate future serialization. With this design pattern fresh in my mind, as I faced the bigger problem of state management, I found myself thinking I should be queuing game state change events rather than just making world changes. This would work, but it also made me realize that all I really wanted was to give a command to the world like "mark two stress on the character and reduce the effect of this consequence." That sounds like a couple of method calls to me.

Casey Yano of MegaCrit (Slay the Spirereflected on his company's evaluation of Godot Engine following the colossal leadership failures at Unity. The sample code he shares uses a combination of a stateful model with asynchronous invocations: await FighterCmd.GainHp(owner, 2, owner). Clearly, he's going through a presentation layer that implements all the fundamental game verbs as asynchronous calls, giving these methods the responsibility to both change the model and display the state change to the user. By contrast, Flutter's declarative approach leans toward having the UI detect a change to the model and then animate the feedback. The latter gives a clear separation of layers that facilitates testing. In practice, though, the game's UI and the game's logic are tightly coupled, and now the code for a feature like "update the health bar when taking damage" is split into disparate places.

In The Endless Storm of Dagger Mountain, which was written in Godot Engine, I managed the state and UI as in Yano's example, writing code like await adventure.show_text('It was a dark and stormy night...'). The use of await makes the call a coroutine, but Godot has no other syntactic indication that a function should be called this way. This means that forgetting a single await will break a chain of intended asynchronous calls, and that's exactly what led to a post-jam patch for that project. I didn't have exhaustive test coverage, nor did I prioritize running through all paths of the game. The result was that a missing await call made at least one of the paths completely lock up for the players. To me, this reflects a weakness of the GDScript language design; by contrast, Dart's use of async, await, and futures make it clear at compile time which invocations are asynchronous and which are not. (Incidentally, Yano is using C# instead of GDScript. I did experiment with GDScript's C# bindings, but a few things held me back from using them: many of the strengths of GDScript, such as elegant signal management, are lost in C#; there is a lot more boilerplate required; there is no Web export for Godot 4.x when using C#; and Rider is so much better than the alternatives, but because is justifiably commercial, it would mean losing money and time to my experiments.)

I had hoped that by this point in my prototyping, I would have a minimal interactive system to which I could focus on adding content and visual flourish. Instead, I have several abandoned experimental architectures. This narrative has been my attempt to explain how I got here. I have learned more about some aspects of Flutter and Dart, but I am also holding two paradoxical ideas in my head: Fred Brooks' observation that you should build a system to throw away because you're going to anyway, and the knowledge that the last 10% of a project takes another 90% of the effort. That is, any understanding I claim to have is on a sandy foundation if the project itself has not shipped. Dagger Mountain may have had critical post-launch patches, but at least I understand exactly why. Whether any particular bloc-like or asynchrony-based approach would be better for this other project is still uncertain. I continue to second-guess myself, but I am also hopeful that having written this, I can return to prototyping after the Christmas break with a fresh perspective.


Monday, December 23, 2024

Happy Camper: A December 2024 FamJam Game

TL;DR: Check out our new game, Happy Camper.

On Saturday, my eldest son led the family in a one-day fam jam as part of fulfilling a Scouting merit badge. We had attended the Indy Indies 2024 Showcase the previous night and played the eight games featured there. Playing Hardcore Cottagecorre in particular got the boys talking about wanting to make a single-stick shooter. The older boys had played Vampire Survivors, but the others really just used Harcore Cottagecorree as their genre example. We laid out responsibilities and got to work a little after 8:00AM. We wrapped up work just before 5:00PM, giving us enough time to talk about our experience over dinner and then get out to see Christmas Carol at Muncie Civic Theatre

The result of our work is Happy Camper, which you can play in the browser as long as you have a keyboard and mouse. The game is free software and you can browse the source code on GitHub.

I told my friend a little about our experience yesterday, and he had some questions about what technology we used to create the game. To that end, here are some explanations and links. These are all no-cost, free, open source tools.

  • We used Godot game engine to build the game, including its GDScript programming language.
  • The art was made using Piskel, which is perfect for the pixel art aesthetic.
  • The music was created using LMMS.
  • The sound effects were recorded using Audacity.
My wife commented on how much smoother these jams go now that everyone has more experience using these various tools. There are two things that I myself need to remember from the experience. One is that I need to talk to the younger boys about how to think like a musician when approaching LMMS. Both have a tendency to try to "make it work" rather than trying to model how composition is approached, doing things like aligning notes to beats and measures. Admittedly, the piano roll interface in LMMS is less clear here than staff, and so maybe I need to look into showing them something like MuseScore or Rosegarden, both of which give you access to a traditional notation editor. 

The other observation I had was with respect to communication, internally and through the medium of game design. My second son took the task of designing a series of weapons that would work well together. He had a list that he considered finished, but I encouraged him to write them up in a way that they could be used as a specification, together with illustrations of how they would work. He did this, but he still described all seven weapons in half a small sketch book page, cramming them all together and including indecipherable drawings of the design intention. We talked briefly about how the task was not merely to inscribe his ideas onto a page, but to do so in a way that invited others to comment, edit, and learn from them. That is, there had to be more room for annotation, more space for people to read the diagrams together. He had succeeded at the "invention" part but was weak on the "communication" part (see Cockburn's argument that software development is a cooperative game of invention and communication). It's all part of the development of teamwork and game design, and I'm glad we had a chance to talk about it. I would like to have an opportunity soon to give him another similar task and see if he can apply our conversation.

This relates to a similar story from later in the day, when he and his elder brother were trying to figure out what to work on before we shipped the game. They seemed blind to the fact that, in its current state, the game was unlearnable and not fun to anyone. As makers of the game, they could play for about ten seconds, and there was no scaffolding for anyone who didn't know all the implementation details. I pushed them on this point, that unless we were making the game only for ourselves, we had to think about the perspective of new users—people who didn't know what the enemies or weapons looked like, where they would come from, or how these systems worked together. Giving them that charge, I left them for about an hour. When they pushed their changes, the game was much more enjoyable, with better balance and escalation without needing massive changes to the implementation. Of course, if we were not in a one-day jam, we could have done even more work here, but within our constraints, I think they did a great job, and I told them so. It was only later that I realized that this was in the same class of feedback as I had given my son earlier: to recognize that "done" needs to be considered from the perspective of the consumer, whether that is the reader of a design document or the player of a game.

It had been almost a year since our last Fam Jam. Some of us will certainly participate in Global Game Jam in January, but I hope it's not another year before we get the whole family involved. I'm not sure what will happen to our Fam Jam tradition once the boys start leaving the house. 

Monday, November 25, 2024

Walking away from a November game project: A reflection on NoGaDeMon 2024, Dart, Flutter, and Bloc

I would hate to make this a tradition, but it seems that I once again entered NoGaDeMon. National Game Design Month (NaGaDeMon) is November, and for several years, I created interesting little projects during the month. Last year, I was not able to pull a project together, and I'm afraid that's the case this year as well. However, I was able to learn a bit through the attempt, so I want to capture some of it here before it slips away.

Before November, I had been tinkering with an intersection of ideas related to posts in the last few months: interactive narrative games like my The Endless Storm of Dagger Mountain, which drew from the Powered by the Apocalypse tabletop RPG space, built around some concepts from Blades in the Dark and Scum & Villainy. I figured that, for November, I would try building a very small slice of the idea. For various reasons, I also wanted to try building and releasing a game using Dart and Flutter. I dug in and started making reasonable progress for a side project.

A few days into November, John Harper released Deep Cuts, a campaign and rules expansion for Blades in the Dark. I bought a copy and was quite surprised at the rules changes. I had expected little tweaks and balancing maneuvers, but Deep Cuts actually provides a complete overhaul of the most fundamental Blades action resolution system. This was too cool not to play with, so I rehashed my planned NaGaDeMon project, essentially starting from scratch to support some of the Deep Cuts ideas.

Before last week, I was able to get a very small version of the game working, letting the player experience a single, badly written game scene. The user-interface was just awful, so in order for the game to come together would have required adding a ton of content and a complete player experience design and implementation. Both of those would be tedious efforts, especially the latter, since I am not very fast with Flutter UI development. Part of the inspiration for choosing Flutter was to gain more practice with engaging UIs. 

About two weeks ago, the work of one of my committees exploded into taking most of my unassigned work hours, and this was not altogether unexpected. We also just got the good news that we will be hosting family for several days around Thanksgiving. This will be wonderful, although it also means these won't be hobby-project days. The result is that I've decided to put this project to rest. I did learn quite a bit going this far into the project, and that is the topic for the remainder of this post.

First of all, the obvious lesson is that if I wanted to really focus on learning to make a top-notch interactive Flutter UI, I should have chosen something with zero other design risks. I knew that the best I could do in one month was to make something just functional, yet I am not sure I was honest with myself about how ugly that would likely end up. Maybe I will find a game jam that will let me get a better handle on combining turn-based game timing with implicit animations.

Prior to November, I had been tinkering with some of these design inspirations in Godot Engine, which is of course the engine I used to build The Endless Storm of Dagger Mountain. I was using a rather conventional mutable-state object-oriented architecture. I found myself frequently frustrated by the lack of good refactoring tools for GDScript. This is a significant hindrance to evolving an appropriate design. This is part of what made me switch over to Dart, which is a joy to work with in part because of the excellent tooling support from Android Studio. 

A few summers ago, I spent a great deal of time studying Filip Hracek's egamebook repository. Nothing shippable came out of my efforts—I don't think I ever even blogged about it—but I did learn a lot. I was struck by how Hracek separated the layers of his architecture, and it was the first time I spent a lot of time in a game that used immutable data models. At the time, I had looked into the Bloc architecture and struggled to make sense out of it.

Approaching this November's project, I decided to dig deeper into Bloc. I spent a lot of time with the official tutorials and puzzling over this seemingly simple diagram:


The simple tutorials are simple, which is convenient, but the more robust ones separate the "data" component into a data provider and repository. It seemed clear that the game state could be conceived of as data, but I struggled to conceptualize where the game rules should live. The game rules can be considered part of the domain model, and as such, should be separated from the bloc. This would mean that a response from the domain model may be the modified game state, which then is echoed back through the bloc to the UI with a bloc state change. However, it's also reasonable to conceive of the game state itself as the data layer and the "business logic" as being the transformations of that state. Indeed, this seems to be the difference between the simple and more complex tutorials: the simple ones deal with simple in-memory state, and the more complex ones draw data from different sources and transform them in the data layer. 

Of course, there is no silver bullet. Given the tight time constraints on the project, I simply considered the immutable game state to be my data layer, and I put the game logic in a bloc. I also simply passed the game state along to the UI, but in a more robust solution, I would have had clearer separation between layers. Including a dependency between the UI and the data layers was a matter of expedience and the intentional incurring of technical debt.

My first pass at the implementation had me writing my game states and bloc states by hand. The Equatable package meant that I didn't have to fret over writing some of the boilerplate that's necessary to do state comparisons, and it was easy to integrate this in Android Studio using Felix Angelov's Bloc plugin. When scouring the Web for help with Bloc, one quickly also comes across discussions of Freezed, which library is also integrated into Angelov's plugin. I had tinkered with Freezed in my egamebook-inspired explorations, but I have not shipped anything that uses it. After having built up my understanding of Bloc using Equatable, Freezed was an obvious next step. Next time, I would jump right into using it for cases like this.

Writing a functional Flutter user-interface was straightforward using BlocBuilder. I found this to be a convenient way to conceptualize the game, especially since it had very clear states. For example, in my original explorations (before Deep Cuts), I had the player choosing an action from a list, then customizing the action with various options from Blades in the Dark, such as pushing yourself to trade stress for dice. After rolling the dice, the player is now in a different state of the game in which they are responding to the result, such as by resisting its consequences. This was elegant to express in the code, and I am confident that with enough effort, I could make a compelling user experience out of it. By contrast, Dagger Mountain used an architecture inspired by MVP but that depended too heavily on the undocumented, unenforceable behavior of coroutines. Both of these are "only jam projects," but they are helping me to conceive of how I would approach something more significant in this problem domain. The aforementioned coroutines were my solution to synchronizing the model and view states (for example, to finish an animation before continuing to the next step of the narrative); I'm fairly certain I understand how I can do that with bloc's events and states, but since the November project will remain unfinished, there is risk.

All this exploratory coding meant that I did not follow a test-driven process. I ended up not getting into the testing libraries specifically for bloc. It's possible that this would have helped me better to conceptualize the business logic versus the domain layer, but that remains future work. 

There are still a lot of questions about the game design itself. Indeed, this entire exploration is inspired by design questions around the adaptation of Blades in the Dark tabletop gameplay into a digital experience. Citizen Sleeper is the only project I know of that has worked in this space, and it's a fantastic interpretation. I only became aware of Citizen Sleeper after I started doodling my own ideas, and it's interesting to see where they converge and where they diverge. I hope to dive back into this design space later, but for now, my attention must go toward wrapping up this semester, planning for next semester, and enjoying the upcoming Thanksgiving break.

Wednesday, November 13, 2024

What people believe you need to do to be an independent game developer

Aspiring game developers are starving for advice. I recently attended a meetup of game developers where an individual gave a formal presentation about how to become an indie. The presentation was thoughtfully crafted and well delivered, and it was entirely structured around imperatives—the things that you, the audience member, need to do if you want to be a successful independent game developer. The audience ate it up and asked for more. They were looking for the golden key that would unlock paradise.

There are two problems here, one overt and one subtle. The overt one is that there is no golden key. There is no set of practices that, if followed, will yield success. I imagine most of the audience knew this and were sifting for gold flakes. However, it was also clearly a mixed crowd, some weathered from years of experience and some fresh-faced hopefuls. I hope the latter were not misled.

The subtler problem was made manifest during the question and answer period when it became clear that the speaker was not actually a successful indie game developer at all. Their singular title had been in development for three years and had just entered beta. They had no actual experience from which to determine if the advice was reasonable or not. The speaker seemed to wholeheartedly believe the advice they were giving despite not being in a position to draw conclusions about their efficacy.

Once I saw the thrust of the presentation, I started taking notes about the kinds of advice the speaker was sharing. 
  •  Document everything, and specifically create:
    • Story and themes document
    • Art and design document
    • MDA document
  • Have a strong creative vision
  • Be a role model for the work environment you want
  • Consider these pro tips for hiring staff:
    • Use a report card to score your candidates
    • Look for ways to get to know what it would be like to work with them
    • Try collaborating with them as part of the interview
    • Always have a back-up candidate, not a top candidate but someone you know you could work with
    • Being their best friend does not mean you should work with them
  • Thank people for their contributions and efforts
  • Use custom tools to help you work better
    • Use the Asset Store in Unity
    • Use tools to help you test
    • Automate as much as you can to save you time
    • Learn to prompt so you can use generative AI
      • It allows an artist to be a developer by removing coding barriers
      • LLMs can replace tedious use of YouTube, Google, Reddit, etc.
  • When pitching to publishers, have two versions of your slide deck:
    • pitch slides: the version you send
    • pitch presentation: the version you present
  • Take budgeting seriously
    • Budget for specific deadlines
    • Don't spend your own money if you can get money from someone else (e.g. publisher)
    • Get a job so that you can support yourself until you can get funding from someone else for the game project
      • Quoting one of his professors: "To make money, you need to spend money, and to spend money, you need money."
  • Don't get distracted by others (e.g. on social media)
These aren't the things you need to do to be an indie game developer. These are the things that an audience believed you need to do to be an indie game developer or the things that someone with a modicum of experience thought would be worth telling indie hopefuls. It seems to me that this is the advice you would get if you spent an afternoon collecting advice by searching the Internet. It's helpful for me to have a list of what people are likely to believe from consuming popular advice. Sometimes advice is popular because it is accurate; sometimes people tell you to make your game state global.

Three other things jumped out at me about the presentation. First was the unspoken assumption that one would be using Unity. There was no indication from the speaker that this was even a choice, and none of the questions reflected on it. Second, the speaker acknowledged the importance of automation and automated testing, which was great to see. Third, no one pushed back regarding the use of CoPilot or other LLMs to help with coding, whereas I suspect there would have been a riot had he suggested using the same tech to generate artwork. There's a study in there.

Friday, June 14, 2024

The Endless Storm of Dagger Mountain: A short adventure game that is Powered by the Apocalypse

Introduction and background

Last night, I released a new game into the wild: The Endless Storm of Dagger Mountain. I submitted it to Crossroads Jam 2024, a statewide game jam sponsored by the Indiana Gamedevs community. 

This game scratches a creative itch that I've had for over two years: what happens when you apply Apocalypse World style rules in a digital game? I've had this as a component of a few different design explorations, none of which bore any fruit—sometimes because they weren't fun and sometimes because their scope exploded. In fact, in May, I started work on a project that was growing too large, and it included PbtA elements. By the last week of May, I had put that side project to rest. I decided that I could use Crossroads Jam as an excuse to isolate just this single design idea—digital PbtA—and package it up into a jam-sized game. Readers may be interested in looking at my previous exploration of tabletop PbtA, which took the form of Kapow! The Campy Superhero Role-Playing Game. I also wrote an essay comparing the math of PbtA and d20 systems.

I was a little disappointed that the theme "severe weather" won the polling on the Indy Gamedevs Discord. Since this is the first Crossroads Jam, it seemed like a great opportunity to highlight something positive about the state. You know, like corn. More seriously, there are a lot of great things about Indiana, including globally-recognized events like the Indianapolis 500. And corn. But I digress, and others preferred "severe weather." I had been wanting to explore some pulp fantasy writing a la Robert E. Howard's Conan stories, which I read a few years ago. This presented a good opportunity: a lone, stoic hero, making a long journey up to the top of a mountain where dark magic has brought about the destruction of the innocent.

Game and Narrative Design

Most of the writing is really just a first draft. Despite years of gamedev, I have done barely any game writing. It felt good to get my hands dirty and create enough content to carry the gameplay. I estimate I spent about 15 hours just writing content for a game that takes a few minutes to play. The writing was enjoyable, but especially as I got tired, I couldn't shake the feeling that much of the text was low stakes. Each dice check has at least three paths—succeed, succeed at a cost, and fail—and I tried to keep them equally interesting. I do not spend a lot of time with interactive fiction, but I quickly ran right into the same problem that any narrative designer has: with chance or with agency comes the loss of authorial control. There are a few specific scenes in the game that I would like to spend more time on, to make the story more compelling and to evoke Howard more strongly.

I was not able to pull in all of the tabletop inspiration that I originally wanted. In tabletop games, I love the idea of the Countdown Clock or what Runehammer calls Timers. A simple timer that ticks down becomes a source of tension for the player and, in the system, it's another formal element to manipulate. In my first draft of Dagger Mountain, I had a timer that, if it ran out, would cause the game to end before the player could summit the mountain. This worked well as a penalty, especially when asking the player to choose between attribute reduction and advancing the timer. However, the game ended up being too short to make the timer meaningful. In fact, one of the problems that inspired me to think about removing the timer was the trouble of visually representing a "timer" as something with only two or three units. I could not reasonably balance it and give it a significant value: a timer with two clicks feels more like it's depleting some other resource rather than feeling like time.

The other common PbtA element that I didn't add was what Apocalypse World calls "reading a sitch." The idea here is that a player can spend an action trying to understand a situation, where success or partial success determines how many questions they can ask about it. Systematically, this is simple: I could have a preconstructed list of questions and answers, and these could provide lore and setting information. As I got into building the narrative, this felt like it would not have a good return on investment: every other decision produced changes in the world state, such as modifications to attributes, and I didn't want "reading the sitch" to be a wellspring of mechanical benefits. It would be relatively easy to add this into my software since it was in mind from the beginning, but it did not find its way into this project.

Speaking of attributes, I still have something of a pipe dream that one could make an RPG attribute system that is consistent with Thomistic philosophy. Consider that the legacy of Dungeons & Dragons presents a sort of dualism, that the mind and the body are separate. Yet, as confirmed by a recent conversation with a weight-lifting friend of mine, the two must work together: it's not clear that Strength (as physical might) and Wisdom (as willpower) are independent variables, for example. I couldn't find a way to distill a more Aristotelian view of the human person into three or four attributes, but reviewing Apocalypse World's attributes, I was reminded that they describe how one does something rather than what someone is. That's a great hook for future design work. In the meantime, for this project, I made a list of actions that I wanted the player to perform, knowing the genre and setting, and I categorized these into the three attributes that are in Dagger Mountain: bold, determined, and savvy. I admit that there are a few stretches in the game where penalties might be hard to classify in these ways, but I will be curious to hear what players think about them as a trio.

Technical Considerations

Inspired by Knights of San Francisco and the beauty of Dart, I started writing Dagger Mountain in Flutter. It's a beautiful way to write applications, but there's a significant difference between declarative and imperative UI programming, and I find that I stutter a bit when I hop between them. I set up the essential architecture and was enjoying myself until I tried to implement some UI features, specifically the scrolling list of text and buttons along with placeholder animations. There is a lot of typing required, animations were hard to debug, and it wasn't always obvious where my problems came from. I am sure there was a lot to learn from the endeavor, but I was on a deadline and wanted to get things up and running quickly. In transitioning back to the comfort of Godot Engine, I realized something: while dart's asynchronous programming features are wonderfully expressive, there is a real power in GDScript's simple signal syntax. It is hard to get more terse than that, although it comes at the cost of not having explicit control over things like Futures. I returned to Godot Engine, re-creating everything I had written in dart in very little time. With the design decisions made, I just had to interpret it in the new environment and type it up.

I spent too much time in Godot Engine adding dynamic font resizing. I knew I wanted the game to run comfortably on a desktop or mobile browser, and giving the player control of font size seemed the best way to do this. It required a lot of shenanigans with theme overrides, and as I added more visual elements such as the visible dice, it got more and more convoluted. Near the end of development, I just gutted this feature from the play experience and put a configuration on the main menu, which allowed me to just fiddle with the values in the main theme rather than deal with distributed theme overrides. What really irked me was when I started working on deployment, realizing that the cleanest solution for the player would be to use the browser's built-in font rendering and resizing... the way that a Flutter app would have done. Sigh.

Here are a few summary observations along these lines. Flutter is great for its static typing, robust asynchronous programming support, autoformatting, built-in browser font resizing support, spread operator, null safety, and most importantly, refactoring support. Godot Engine clearly wins on terseness of signals and tweens and the ability to rapidly build and test scenes independently of each other. To clarify that last point, I regularly decompose my Godot Engine programs into scenes that I can run and configure by themselves, confident then in how they will work when instantiated as part of a larger system. In Flutter, I wish I could easily say, "Spin up one of these widgets by itself and let me tinker with it," but I have not found anything that comes close to Godot Engine's rapid development support this way.

Incidentally, I did briefly consider other options than writing my own engine. I am intrigued particularly by ink, which I have never used. I was hesitant to jump into something with such a different syntax, although I am sure I could learn a lot from it, too. What killed the deal for me though was that it wasn't clear to me that I could easily plug in the PbtA aspects that I wanted. I discovered a Godot Engine integration, so perhaps I will investigate that later this summer. It wasn't until my family was testing the game that one of them mentioned Dialogic, which I haven't used since Godot 3.x. I haven't looked at it to see if it could have been modded for my purpose. However, writing for Dagger Mountain made me appreciate why narrative designers need better tools than just piles of scripts and a notebook sketch.

Dagger Mountain is my first released game that uses an event queue to isolate the game rules from the interface. I have tinkered with this pattern in several abandoned projects. Two summers ago, I spent a lot of time studying egamebook and its architecture, and I learned a lot from it even though that particular summer project was never released. 

My approach separates the software into three parts. The module is the content of the adventure itself, the story of Dagger Mountain. Each scene in Dagger Mountain is a GDScript file that is given a reference to an Adventure object. The next part is the rules engine, which is manifest in an Adventure object. The scene is given a reference to an Adventure object, and the module tells it to do things like show text, modify attributes, or present a series of choices to the player. Internally, the rules engine generates events to correspond to these interactions, posting them to the event queue. The final part is the presentation layer, which subscribes to the event queue. It dequeues events, processes them, and then notifies the rules engine when it is complete. The code is all free, so feel free to look at the prelude scene for an example of how this works.

I decided early in the project that I would repurpose GDScript as my narrative scripting language rather than create an independent data format that would be interpreted. The primary reason for this decision was the pressures of time: GDScript is already a scripting language, so using its support for functions and conditionals would be faster than writing my own. This is true, but I hadn't considered all of the costs at the time. The game runs through function calls in the module layer, which is nice for terseness but actually makes it hard to test in a modular fashion. I am sure that if I had used TDD, I would have had a more testable architecture. I would much rather have test coverage of the whole state space of the game; instead, I have to hope that my manual testing was adequate.

I did add integration tests near the end because of the need to await basically every call in the module layer. Missing a single instance will break the player experience. I wrote a test that reads through the module layer and looks for cases where await is missing. It took a little time get the test working, but it immediately found a case that I had missed, so that was worthwhile.

Conclusions

I enjoyed building The Endless Storm of Dagger Mountain, and I hope you enjoy playing it. I think I will go and tweak some of that text with this morning's remaining coffee. Despite its small scope and shortcomings, I feel good about having built it. Not only does it explore digital PbtA in a way that I've been imagining for a few years, it also gave me an opportunity to do some creative writing and build more empathy for narrative designers. 

Regarding digital PbtA, I think the jury is still out, since for every promise it has, it comes at the cost of dramatic increase in content creation costs. For interactive fiction, using PbtA resolution requires writing an enormous amount of text, much of which will never been seen my players. It is, of course, not the same feeling as the give and take of a tabletop RPG. However, I can see opportunities for using this resolution system if there were more supporting systems. In a larger game, for example, one could put back in "reading the sitch" style actions that give clues to puzzles. I prefer losing attribute points over the abstraction of hit points, particularly for a narrative-focused game, but I think this would benefit from more explicit representation. Gaining statuses like "confused" or "twisted ankle" would help carry the narrative forward, but then these would be most meaningful if worked into the other systems or stories of the game. All that being said, I appreciate how the PbtA elements feel more like a description of a whole human person than do hit points, armor classes, and the six classic attributes.

Thanks for reading. Let me know what you think of the game!